File size: 5,031 Bytes
a0b8bc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# -*- coding: utf-8 -*-
"""

padding_patch.py — Controla EXPLICITAMENTE a cor de preenchimento (padding) do

Ultralytics, para a ablation black-edge vs white-edge.



Por que e necessario: o Ultralytics 8.4.x codifica 114 (cinza) em TRES lugares

independentes, nenhum exposto no cfg de treino:

  1. Mosaic._mosaic4/_mosaic9 -> np.full(..., 114)          [treino]

  2. RandomPerspective        -> borderValue=(114,114,114)  [treino]

  3. LetterBox                -> padding_value=114          [val/inferencia]



Patchear so o LetterBox contaminaria a ablation (o treino seguiria cinza).

Este modulo intercepta os tres de forma auditavel e verificavel.



Uso:

    import padding_patch; padding_patch.apply("black")   # 0

    import padding_patch; padding_patch.apply("white")   # 255

    import padding_patch; padding_patch.apply("gray")    # 114 (default upstream)

"""
import os

import numpy as np

VALUES = {"black": 0, "white": 255, "gray": 114}
ENV = "WHST_PADDING_MODE"
_APPLIED = None


def worker_init(worker_id: int) -> None:
    """worker_init_fn que REAPLICA o patch dentro de cada worker process.



    ISTO E O CORACAO DA CORRECAO — sem isto a ablation nao acontece.



    No Windows o DataLoader usa SPAWN: cada worker e um processo novo, que nao

    herda monkeypatch nenhum do pai. Diagnosticado em 2026-07-27: os workers

    reportavam `np.full(114) -> 114` enquanto o pai reportava 255, e as 100

    epocas de treino de black e white sairam com loss BIT-IDENTICA.



    O LetterBox escapava do problema por acidente — o padding_value vira atributo

    de instancia no __init__ (executado no pai) e viaja no pickle do dataset. Por

    isso a VALIDACAO diferia e o TREINO nao: exatamente o padrao observado.



    Esta funcao e passada como `worker_init_fn`. Ela e picklada POR REFERENCIA

    (`padding_patch.worker_init`), o que obriga o worker a importar este modulo —

    e entao o patch e aplicado la dentro, antes do primeiro batch. O modo vem da

    variavel de ambiente, que processos filhos herdam.

    """
    modo = os.environ.get(ENV)
    if modo in VALUES:
        apply(modo, _em_worker=True)
    # preserva o comportamento original do Ultralytics
    try:
        import random

        import torch
        s = torch.initial_seed() % 2 ** 32
        np.random.seed(s)
        random.seed(s)
    except Exception:
        pass


def apply(mode: str, _em_worker: bool = False):
    """Aplica o valor de padding globalmente. Retorna o valor numerico usado."""
    global _APPLIED
    if mode not in VALUES:
        raise ValueError(f"modo invalido: {mode} (use {list(VALUES)})")
    fill = VALUES[mode]
    os.environ[ENV] = mode          # herdado pelos processos filhos (spawn)

    import ultralytics.data.augment as A

    # ---- 1) LetterBox: forca o default de padding_value ----
    if not hasattr(A.LetterBox, "_orig_init"):
        A.LetterBox._orig_init = A.LetterBox.__init__

    def _lb_init(self, *args, **kw):
        kw["padding_value"] = fill
        return A.LetterBox._orig_init(self, *args, **kw)

    A.LetterBox.__init__ = _lb_init

    # ---- 2) RandomPerspective: borderValue do warp ----
    cv2 = A.cv2
    if not hasattr(cv2, "_orig_warpAffine"):
        cv2._orig_warpAffine = cv2.warpAffine
        cv2._orig_warpPerspective = cv2.warpPerspective

    # guard POR VALOR (==114), simetrico ao _full: so intercepta o preenchimento
    # cinza da imagem em RandomPerspective. NAO toca o borderValue=255 usado por
    # apply_semantic para a mascara semantica (latente neste pipeline, mas blinda).
    def _wa(src, M, dsize, *a, **kw):
        if kw.get("borderValue") == (114, 114, 114):
            kw["borderValue"] = (fill,) * 3
        return cv2._orig_warpAffine(src, M, dsize, *a, **kw)

    def _wp(src, M, dsize, *a, **kw):
        if kw.get("borderValue") == (114, 114, 114):
            kw["borderValue"] = (fill,) * 3
        return cv2._orig_warpPerspective(src, M, dsize, *a, **kw)

    cv2.warpAffine = _wa
    cv2.warpPerspective = _wp

    # ---- 3) Mosaic: canvas np.full(..., 114) ----
    if not hasattr(A.np, "_orig_full"):
        A.np._orig_full = np.full

    def _full(shape, fill_value, *a, **kw):
        # so intercepta o canvas cinza do mosaico; nao mexe em outros np.full
        if isinstance(fill_value, (int, np.integer)) and int(fill_value) == 114:
            fill_value = fill
        return A.np._orig_full(shape, fill_value, *a, **kw)

    A.np.full = _full

    # ---- 4) worker_init_fn: leva o patch para dentro dos workers ----
    # Sem isto, os itens 2 e 3 nao existem no processo que de fato monta o batch.
    if not _em_worker:
        import ultralytics.data.build as B
        if getattr(B, "_padding_worker_init", None) is not worker_init:
            B.seed_worker = worker_init
            B._padding_worker_init = worker_init

    _APPLIED = mode
    return fill


def applied():
    return _APPLIED