---
name: crop_to_content
kind: function
lang: py
domain: ml
version: "1.0.0"
purity: pure
signature: "def crop_to_content(img, *, pad_ratio: float = 0.06, square: bool = True, alpha_threshold: int = 10, bg_tolerance: int = 16)"
description: "Recorta una imagen PIL al bounding box de su contenido y la cuadra, para que el sujeto llene el frame antes de un downscale a pixel-art. Detecta el contenido por alpha (region con alpha > alpha_threshold) si la imagen es RGBA/LA, o por diferencia contra el color de fondo de las esquinas (con bg_tolerance) si es RGB. Recorta al bbox, anade un margen pad_ratio y, si square, rellena a cuadrado centrando el sujeto sin deformar (fondo transparente si RGBA, color de fondo si RGB). Pura PIL (opera sobre el objeto PIL.Image, no toca disco ni red, no muta la entrada). Si no hay contenido (todo transparente o todo fondo) devuelve una copia intacta — no crashea."
tags: [pil, image, crop, bbox, pixelart, gamedev-2d, ml, alpha, sprite]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: []
params:
  - name: img
    desc: "PIL.Image de entrada (cualquier modo). No se muta. None lanza ValueError."
  - name: pad_ratio
    desc: "Margen anadido alrededor del sujeto como fraccion del lado mayor del bbox recortado (0.06 = 6%). 0 = sin margen. keyword-only."
  - name: square
    desc: "Si True rellena a un lienzo cuadrado de lado max(w,h)+2*pad con el sujeto centrado (fondo transparente si hay alpha, color de fondo si RGB); si False solo recorta al bbox + margen sin cuadrar. keyword-only."
  - name: alpha_threshold
    desc: "Umbral de alpha (0..255) para considerar un pixel 'contenido' cuando la imagen tiene canal alpha. keyword-only."
  - name: bg_tolerance
    desc: "Tolerancia (0..255) de diferencia contra el color de fondo de las esquinas para imagenes sin alpha (RGB). keyword-only."
output: "PIL.Image nueva recortada (y cuadrada si square) con el sujeto llenando el frame. Si la imagen no tiene contenido detectable, devuelve una copia intacta de la entrada (mismo tamano)."
tested: true
tests: [test_golden_corner_subject_fills_frame, test_edge_centered_subject_not_overcropped, test_edge_rgb_background_bbox, test_edge_no_square_only_crops, test_error_all_transparent_returns_copy, test_error_none_raises, test_does_not_mutate_input]
test_file_path: "python/functions/ml/crop_to_content_test.py"
file_path: "python/functions/ml/crop_to_content.py"
---

## Ejemplo

```python
import sys, os
sys.path.insert(0, os.path.join(os.environ["HOME"], "fn_registry", "python", "functions"))
from PIL import Image
from ml.crop_to_content import crop_to_content

# Sprite RGBA tras rembg: el sujeto ocupa una esquina -> recortar al bbox y cuadrar.
with Image.open("/tmp/knight_rgba.png") as im:
    out = crop_to_content(im, pad_ratio=0.06, square=True)
out.save("/tmp/knight_cropped.png")   # RGBA cuadrada, sujeto centrado llenando el frame

# CLI directo:
#   ./fn run crop_to_content   (corre los tests)
#   python3 crop_to_content.py /tmp/in.png /tmp/out.png 0.06
```

## Cuando usarla

Antes de bajar una imagen a pixel-art (32/64px): si el sujeto ocupa poca area del
lienzo, al downscalear queda diminuto y tosco. `crop_to_content` recorta el aire
alrededor y cuadra para que el sujeto aproveche todos los pixeles del grid. Es el
paso de encuadre del pipeline `comfyui_pixelart_real_oneshot` (autocrop). Funciona
con sprites recortados por rembg (detecta por alpha) o con imagenes de fondo plano
(detecta por diferencia contra el color de esquina).

## Gotchas

- **Pura sobre PIL.Image**: recibe y devuelve un objeto `PIL.Image`, NO rutas. El
  caller hace el `Image.open` / `.save`. No muta la imagen de entrada.
- Deteccion del contenido: con **alpha** usa `alpha > alpha_threshold`; sin alpha
  usa la **moda de las 4 esquinas** como color de fondo y `bg_tolerance` de
  diferencia. Si el fondo no es uniforme (gradiente) la deteccion RGB puede fallar;
  para esos casos pasa la imagen ya recortada por rembg (RGBA).
- Si no hay contenido (todo transparente o todo del color de fondo) devuelve una
  **copia intacta** del original (mismo tamano), nunca lanza por una imagen vacia.
  Solo lanza `ValueError` si `img` es `None`.
- `square=True` (default) cuadra a `max(w,h)+2*pad`: si el sujeto es muy alargado el
  lienzo crece al lado mayor y el sujeto queda centrado con barras transparentes (o
  de color de fondo) a los lados — sin deformar.
- `pad_ratio` es relativo al lado **mayor del bbox**, no del lienzo original.
