"""Tests de crop_to_content (offline, sin red ni GPU; PIL/numpy).""" import os import sys import numpy as np from PIL import Image sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from ml.crop_to_content import crop_to_content # noqa: E402 def _rgba_subject_in_corner(canvas=256, box=40, ox=8, oy=8): """RGBA con un rectangulo opaco rojo en una esquina, resto transparente.""" arr = np.zeros((canvas, canvas, 4), dtype=np.uint8) arr[oy:oy + box, ox:ox + box, 0] = 220 # R arr[oy:oy + box, ox:ox + box, 3] = 255 # alpha opaco return Image.fromarray(arr, "RGBA") def _rgba_subject_centered(canvas=256, fill_ratio=0.9): """RGBA con un rectangulo opaco que llena ~fill_ratio del lienzo, centrado.""" arr = np.zeros((canvas, canvas, 4), dtype=np.uint8) side = int(canvas * fill_ratio) o = (canvas - side) // 2 arr[o:o + side, o:o + side, 1] = 200 # G arr[o:o + side, o:o + side, 3] = 255 return Image.fromarray(arr, "RGBA") def _rgb_subject_on_bg(canvas=200, box=50, ox=10, oy=10, bg=(255, 255, 255)): """RGB con un cuadrado de color sobre fondo plano (sin alpha).""" arr = np.zeros((canvas, canvas, 3), dtype=np.uint8) arr[:, :] = bg arr[oy:oy + box, ox:ox + box] = (0, 0, 200) # sujeto azul return Image.fromarray(arr, "RGB") def _alpha_bbox_coverage(img, threshold=10): """Fraccion del lado que ocupa el bbox del contenido (alpha>threshold).""" a = np.asarray(img.convert("RGBA"))[..., 3] ys, xs = np.where(a > threshold) if xs.size == 0: return 0.0 bw = xs.max() - xs.min() + 1 bh = ys.max() - ys.min() + 1 return max(bw, bh) / max(img.size) def test_golden_corner_subject_fills_frame(): """Sujeto en la esquina -> tras crop ocupa casi todo el frame (square).""" img = _rgba_subject_in_corner() before = _alpha_bbox_coverage(img) out = crop_to_content(img, pad_ratio=0.06, square=True) after = _alpha_bbox_coverage(out) assert out.mode == "RGBA" assert out.size[0] == out.size[1] # cuadrado assert before < 0.25 # antes diminuto assert after >= 0.80 # despues llena el frame def test_edge_centered_subject_not_overcropped(): """Sujeto ya centrado que llena ~90%: la cobertura se mantiene alta, no se rompe.""" img = _rgba_subject_centered(fill_ratio=0.9) out = crop_to_content(img, pad_ratio=0.06, square=True) assert out.size[0] == out.size[1] assert _alpha_bbox_coverage(out) >= 0.80 def test_edge_rgb_background_bbox(): """RGB con fondo plano: detecta el sujeto por diff-fondo y lo cuadra.""" img = _rgb_subject_on_bg() out = crop_to_content(img, pad_ratio=0.05, square=True) assert out.mode == "RGB" assert out.size[0] == out.size[1] # El sujeto azul debe ocupar buena parte del lienzo recortado. arr = np.asarray(out) is_subject = (arr[..., 2] > 120) & (arr[..., 0] < 80) cov = is_subject.sum() / (out.size[0] * out.size[1]) assert cov >= 0.4 def test_edge_no_square_only_crops(): """square=False: recorta al bbox + margen, sin forzar cuadrado.""" img = _rgba_subject_in_corner(box=40) out = crop_to_content(img, pad_ratio=0.0, square=False) # bbox del sujeto es 40x40 -> sin pad ni cuadrar, sale 40x40. assert out.size == (40, 40) def test_error_all_transparent_returns_copy(): """Imagen toda transparente: no crashea, devuelve copia intacta (mismo tamano).""" arr = np.zeros((128, 128, 4), dtype=np.uint8) # alpha 0 en todo img = Image.fromarray(arr, "RGBA") out = crop_to_content(img) assert out.size == (128, 128) assert np.asarray(out)[..., 3].max() == 0 def test_error_none_raises(): try: crop_to_content(None) assert False, "deberia lanzar ValueError" except ValueError as e: assert "None" in str(e) def test_does_not_mutate_input(): img = _rgba_subject_in_corner() snapshot = np.asarray(img).copy() crop_to_content(img) assert np.array_equal(np.asarray(img), snapshot)