Scratch-assay-segmentation / padding_patch.py
nmariotto's picture
Upload 2 files
a0b8bc0 verified
Raw
History Blame Contribute Delete
5.03 kB
# -*- 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