Upload 2 files
Browse files- app.py +3 -3
- padding_patch.py +130 -130
app.py
CHANGED
|
@@ -72,9 +72,9 @@ MODEL_INFO = {
|
|
| 72 |
def _padding_efetivo():
|
| 73 |
"""Proves the padding reached LetterBox instead of assuming it did.
|
| 74 |
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
"""
|
| 79 |
import ultralytics.data.augment as A
|
| 80 |
return getattr(A.LetterBox(new_shape=(640, 640)), "padding_value", None)
|
|
|
|
| 72 |
def _padding_efetivo():
|
| 73 |
"""Proves the padding reached LetterBox instead of assuming it did.
|
| 74 |
|
| 75 |
+
A patch that is assumed to have applied, and has not, produces plausible
|
| 76 |
+
numbers rather than an error — which is the hardest kind of defect to notice.
|
| 77 |
+
The check costs one line, so it is made rather than assumed.
|
| 78 |
"""
|
| 79 |
import ultralytics.data.augment as A
|
| 80 |
return getattr(A.LetterBox(new_shape=(640, 640)), "padding_value", None)
|
padding_patch.py
CHANGED
|
@@ -1,130 +1,130 @@
|
|
| 1 |
-
# -*- coding: utf-8 -*-
|
| 2 |
-
"""
|
| 3 |
-
padding_patch.py — Controla EXPLICITAMENTE a cor de preenchimento (padding) do
|
| 4 |
-
Ultralytics, para a ablation black-edge vs white-edge
|
| 5 |
-
|
| 6 |
-
Por que e necessario: o Ultralytics 8.4.x codifica 114 (cinza) em TRES lugares
|
| 7 |
-
independentes, nenhum exposto no cfg de treino:
|
| 8 |
-
1. Mosaic._mosaic4/_mosaic9 -> np.full(..., 114) [treino]
|
| 9 |
-
2. RandomPerspective -> borderValue=(114,114,114) [treino]
|
| 10 |
-
3. LetterBox -> padding_value=114 [val/inferencia]
|
| 11 |
-
|
| 12 |
-
Patchear so o LetterBox contaminaria a ablation (o treino seguiria cinza).
|
| 13 |
-
Este modulo intercepta os tres de forma auditavel e verificavel.
|
| 14 |
-
|
| 15 |
-
Uso:
|
| 16 |
-
import padding_patch; padding_patch.apply("black") # 0
|
| 17 |
-
import padding_patch; padding_patch.apply("white") # 255
|
| 18 |
-
import padding_patch; padding_patch.apply("gray") # 114 (default upstream)
|
| 19 |
-
"""
|
| 20 |
-
import os
|
| 21 |
-
|
| 22 |
-
import numpy as np
|
| 23 |
-
|
| 24 |
-
VALUES = {"black": 0, "white": 255, "gray": 114}
|
| 25 |
-
ENV = "WHST_PADDING_MODE"
|
| 26 |
-
_APPLIED = None
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def worker_init(worker_id: int) -> None:
|
| 30 |
-
"""worker_init_fn que REAPLICA o patch dentro de cada worker process.
|
| 31 |
-
|
| 32 |
-
ISTO E O CORACAO DA CORRECAO — sem isto a ablation nao acontece.
|
| 33 |
-
|
| 34 |
-
No Windows o DataLoader usa SPAWN: cada worker e um processo novo, que nao
|
| 35 |
-
herda monkeypatch nenhum do pai. Diagnosticado em 2026-07-27: os workers
|
| 36 |
-
reportavam `np.full(114) -> 114` enquanto o pai reportava 255, e as 100
|
| 37 |
-
epocas de treino de black e white sairam com loss BIT-IDENTICA.
|
| 38 |
-
|
| 39 |
-
O LetterBox escapava do problema por acidente — o padding_value vira atributo
|
| 40 |
-
de instancia no __init__ (executado no pai) e viaja no pickle do dataset. Por
|
| 41 |
-
isso a VALIDACAO diferia e o TREINO nao: exatamente o padrao observado.
|
| 42 |
-
|
| 43 |
-
Esta funcao e passada como `worker_init_fn`. Ela e picklada POR REFERENCIA
|
| 44 |
-
(`padding_patch.worker_init`), o que obriga o worker a importar este modulo —
|
| 45 |
-
e entao o patch e aplicado la dentro, antes do primeiro batch. O modo vem da
|
| 46 |
-
variavel de ambiente, que processos filhos herdam.
|
| 47 |
-
"""
|
| 48 |
-
modo = os.environ.get(ENV)
|
| 49 |
-
if modo in VALUES:
|
| 50 |
-
apply(modo, _em_worker=True)
|
| 51 |
-
# preserva o comportamento original do Ultralytics
|
| 52 |
-
try:
|
| 53 |
-
import random
|
| 54 |
-
|
| 55 |
-
import torch
|
| 56 |
-
s = torch.initial_seed() % 2 ** 32
|
| 57 |
-
np.random.seed(s)
|
| 58 |
-
random.seed(s)
|
| 59 |
-
except Exception:
|
| 60 |
-
pass
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
def apply(mode: str, _em_worker: bool = False):
|
| 64 |
-
"""Aplica o valor de padding globalmente. Retorna o valor numerico usado."""
|
| 65 |
-
global _APPLIED
|
| 66 |
-
if mode not in VALUES:
|
| 67 |
-
raise ValueError(f"modo invalido: {mode} (use {list(VALUES)})")
|
| 68 |
-
fill = VALUES[mode]
|
| 69 |
-
os.environ[ENV] = mode # herdado pelos processos filhos (spawn)
|
| 70 |
-
|
| 71 |
-
import ultralytics.data.augment as A
|
| 72 |
-
|
| 73 |
-
# ---- 1) LetterBox: forca o default de padding_value ----
|
| 74 |
-
if not hasattr(A.LetterBox, "_orig_init"):
|
| 75 |
-
A.LetterBox._orig_init = A.LetterBox.__init__
|
| 76 |
-
|
| 77 |
-
def _lb_init(self, *args, **kw):
|
| 78 |
-
kw["padding_value"] = fill
|
| 79 |
-
return A.LetterBox._orig_init(self, *args, **kw)
|
| 80 |
-
|
| 81 |
-
A.LetterBox.__init__ = _lb_init
|
| 82 |
-
|
| 83 |
-
# ---- 2) RandomPerspective: borderValue do warp ----
|
| 84 |
-
cv2 = A.cv2
|
| 85 |
-
if not hasattr(cv2, "_orig_warpAffine"):
|
| 86 |
-
cv2._orig_warpAffine = cv2.warpAffine
|
| 87 |
-
cv2._orig_warpPerspective = cv2.warpPerspective
|
| 88 |
-
|
| 89 |
-
# guard POR VALOR (==114), simetrico ao _full: so intercepta o preenchimento
|
| 90 |
-
# cinza da imagem em RandomPerspective. NAO toca o borderValue=255 usado por
|
| 91 |
-
# apply_semantic para a mascara semantica (latente neste pipeline, mas blinda).
|
| 92 |
-
def _wa(src, M, dsize, *a, **kw):
|
| 93 |
-
if kw.get("borderValue") == (114, 114, 114):
|
| 94 |
-
kw["borderValue"] = (fill,) * 3
|
| 95 |
-
return cv2._orig_warpAffine(src, M, dsize, *a, **kw)
|
| 96 |
-
|
| 97 |
-
def _wp(src, M, dsize, *a, **kw):
|
| 98 |
-
if kw.get("borderValue") == (114, 114, 114):
|
| 99 |
-
kw["borderValue"] = (fill,) * 3
|
| 100 |
-
return cv2._orig_warpPerspective(src, M, dsize, *a, **kw)
|
| 101 |
-
|
| 102 |
-
cv2.warpAffine = _wa
|
| 103 |
-
cv2.warpPerspective = _wp
|
| 104 |
-
|
| 105 |
-
# ---- 3) Mosaic: canvas np.full(..., 114) ----
|
| 106 |
-
if not hasattr(A.np, "_orig_full"):
|
| 107 |
-
A.np._orig_full = np.full
|
| 108 |
-
|
| 109 |
-
def _full(shape, fill_value, *a, **kw):
|
| 110 |
-
# so intercepta o canvas cinza do mosaico; nao mexe em outros np.full
|
| 111 |
-
if isinstance(fill_value, (int, np.integer)) and int(fill_value) == 114:
|
| 112 |
-
fill_value = fill
|
| 113 |
-
return A.np._orig_full(shape, fill_value, *a, **kw)
|
| 114 |
-
|
| 115 |
-
A.np.full = _full
|
| 116 |
-
|
| 117 |
-
# ---- 4) worker_init_fn: leva o patch para dentro dos workers ----
|
| 118 |
-
# Sem isto, os itens 2 e 3 nao existem no processo que de fato monta o batch.
|
| 119 |
-
if not _em_worker:
|
| 120 |
-
import ultralytics.data.build as B
|
| 121 |
-
if getattr(B, "_padding_worker_init", None) is not worker_init:
|
| 122 |
-
B.seed_worker = worker_init
|
| 123 |
-
B._padding_worker_init = worker_init
|
| 124 |
-
|
| 125 |
-
_APPLIED = mode
|
| 126 |
-
return fill
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
def applied():
|
| 130 |
-
return _APPLIED
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
padding_patch.py — Controla EXPLICITAMENTE a cor de preenchimento (padding) do
|
| 4 |
+
Ultralytics, para a ablation black-edge vs white-edge.
|
| 5 |
+
|
| 6 |
+
Por que e necessario: o Ultralytics 8.4.x codifica 114 (cinza) em TRES lugares
|
| 7 |
+
independentes, nenhum exposto no cfg de treino:
|
| 8 |
+
1. Mosaic._mosaic4/_mosaic9 -> np.full(..., 114) [treino]
|
| 9 |
+
2. RandomPerspective -> borderValue=(114,114,114) [treino]
|
| 10 |
+
3. LetterBox -> padding_value=114 [val/inferencia]
|
| 11 |
+
|
| 12 |
+
Patchear so o LetterBox contaminaria a ablation (o treino seguiria cinza).
|
| 13 |
+
Este modulo intercepta os tres de forma auditavel e verificavel.
|
| 14 |
+
|
| 15 |
+
Uso:
|
| 16 |
+
import padding_patch; padding_patch.apply("black") # 0
|
| 17 |
+
import padding_patch; padding_patch.apply("white") # 255
|
| 18 |
+
import padding_patch; padding_patch.apply("gray") # 114 (default upstream)
|
| 19 |
+
"""
|
| 20 |
+
import os
|
| 21 |
+
|
| 22 |
+
import numpy as np
|
| 23 |
+
|
| 24 |
+
VALUES = {"black": 0, "white": 255, "gray": 114}
|
| 25 |
+
ENV = "WHST_PADDING_MODE"
|
| 26 |
+
_APPLIED = None
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def worker_init(worker_id: int) -> None:
|
| 30 |
+
"""worker_init_fn que REAPLICA o patch dentro de cada worker process.
|
| 31 |
+
|
| 32 |
+
ISTO E O CORACAO DA CORRECAO — sem isto a ablation nao acontece.
|
| 33 |
+
|
| 34 |
+
No Windows o DataLoader usa SPAWN: cada worker e um processo novo, que nao
|
| 35 |
+
herda monkeypatch nenhum do pai. Diagnosticado em 2026-07-27: os workers
|
| 36 |
+
reportavam `np.full(114) -> 114` enquanto o pai reportava 255, e as 100
|
| 37 |
+
epocas de treino de black e white sairam com loss BIT-IDENTICA.
|
| 38 |
+
|
| 39 |
+
O LetterBox escapava do problema por acidente — o padding_value vira atributo
|
| 40 |
+
de instancia no __init__ (executado no pai) e viaja no pickle do dataset. Por
|
| 41 |
+
isso a VALIDACAO diferia e o TREINO nao: exatamente o padrao observado.
|
| 42 |
+
|
| 43 |
+
Esta funcao e passada como `worker_init_fn`. Ela e picklada POR REFERENCIA
|
| 44 |
+
(`padding_patch.worker_init`), o que obriga o worker a importar este modulo —
|
| 45 |
+
e entao o patch e aplicado la dentro, antes do primeiro batch. O modo vem da
|
| 46 |
+
variavel de ambiente, que processos filhos herdam.
|
| 47 |
+
"""
|
| 48 |
+
modo = os.environ.get(ENV)
|
| 49 |
+
if modo in VALUES:
|
| 50 |
+
apply(modo, _em_worker=True)
|
| 51 |
+
# preserva o comportamento original do Ultralytics
|
| 52 |
+
try:
|
| 53 |
+
import random
|
| 54 |
+
|
| 55 |
+
import torch
|
| 56 |
+
s = torch.initial_seed() % 2 ** 32
|
| 57 |
+
np.random.seed(s)
|
| 58 |
+
random.seed(s)
|
| 59 |
+
except Exception:
|
| 60 |
+
pass
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def apply(mode: str, _em_worker: bool = False):
|
| 64 |
+
"""Aplica o valor de padding globalmente. Retorna o valor numerico usado."""
|
| 65 |
+
global _APPLIED
|
| 66 |
+
if mode not in VALUES:
|
| 67 |
+
raise ValueError(f"modo invalido: {mode} (use {list(VALUES)})")
|
| 68 |
+
fill = VALUES[mode]
|
| 69 |
+
os.environ[ENV] = mode # herdado pelos processos filhos (spawn)
|
| 70 |
+
|
| 71 |
+
import ultralytics.data.augment as A
|
| 72 |
+
|
| 73 |
+
# ---- 1) LetterBox: forca o default de padding_value ----
|
| 74 |
+
if not hasattr(A.LetterBox, "_orig_init"):
|
| 75 |
+
A.LetterBox._orig_init = A.LetterBox.__init__
|
| 76 |
+
|
| 77 |
+
def _lb_init(self, *args, **kw):
|
| 78 |
+
kw["padding_value"] = fill
|
| 79 |
+
return A.LetterBox._orig_init(self, *args, **kw)
|
| 80 |
+
|
| 81 |
+
A.LetterBox.__init__ = _lb_init
|
| 82 |
+
|
| 83 |
+
# ---- 2) RandomPerspective: borderValue do warp ----
|
| 84 |
+
cv2 = A.cv2
|
| 85 |
+
if not hasattr(cv2, "_orig_warpAffine"):
|
| 86 |
+
cv2._orig_warpAffine = cv2.warpAffine
|
| 87 |
+
cv2._orig_warpPerspective = cv2.warpPerspective
|
| 88 |
+
|
| 89 |
+
# guard POR VALOR (==114), simetrico ao _full: so intercepta o preenchimento
|
| 90 |
+
# cinza da imagem em RandomPerspective. NAO toca o borderValue=255 usado por
|
| 91 |
+
# apply_semantic para a mascara semantica (latente neste pipeline, mas blinda).
|
| 92 |
+
def _wa(src, M, dsize, *a, **kw):
|
| 93 |
+
if kw.get("borderValue") == (114, 114, 114):
|
| 94 |
+
kw["borderValue"] = (fill,) * 3
|
| 95 |
+
return cv2._orig_warpAffine(src, M, dsize, *a, **kw)
|
| 96 |
+
|
| 97 |
+
def _wp(src, M, dsize, *a, **kw):
|
| 98 |
+
if kw.get("borderValue") == (114, 114, 114):
|
| 99 |
+
kw["borderValue"] = (fill,) * 3
|
| 100 |
+
return cv2._orig_warpPerspective(src, M, dsize, *a, **kw)
|
| 101 |
+
|
| 102 |
+
cv2.warpAffine = _wa
|
| 103 |
+
cv2.warpPerspective = _wp
|
| 104 |
+
|
| 105 |
+
# ---- 3) Mosaic: canvas np.full(..., 114) ----
|
| 106 |
+
if not hasattr(A.np, "_orig_full"):
|
| 107 |
+
A.np._orig_full = np.full
|
| 108 |
+
|
| 109 |
+
def _full(shape, fill_value, *a, **kw):
|
| 110 |
+
# so intercepta o canvas cinza do mosaico; nao mexe em outros np.full
|
| 111 |
+
if isinstance(fill_value, (int, np.integer)) and int(fill_value) == 114:
|
| 112 |
+
fill_value = fill
|
| 113 |
+
return A.np._orig_full(shape, fill_value, *a, **kw)
|
| 114 |
+
|
| 115 |
+
A.np.full = _full
|
| 116 |
+
|
| 117 |
+
# ---- 4) worker_init_fn: leva o patch para dentro dos workers ----
|
| 118 |
+
# Sem isto, os itens 2 e 3 nao existem no processo que de fato monta o batch.
|
| 119 |
+
if not _em_worker:
|
| 120 |
+
import ultralytics.data.build as B
|
| 121 |
+
if getattr(B, "_padding_worker_init", None) is not worker_init:
|
| 122 |
+
B.seed_worker = worker_init
|
| 123 |
+
B._padding_worker_init = worker_init
|
| 124 |
+
|
| 125 |
+
_APPLIED = mode
|
| 126 |
+
return fill
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def applied():
|
| 130 |
+
return _APPLIED
|