Añadir mu-plugins y scripts de feadulta

This commit is contained in:
2026-06-28 15:10:46 -04:00
parent bce7e42f44
commit b6116b066d
106 changed files with 17600 additions and 2 deletions
+142
View File
@@ -0,0 +1,142 @@
#!/usr/bin/env python3
"""
Recorta avatares cuadrados centrados en la cara, usando Haar cascade de OpenCV.
Uso:
python3 face_crop_avatar.py <src> <dst> [--size 256] [--padding 0.6]
python3 face_crop_avatar.py --batch <src_dir> <dst_dir> [--size 256]
Strategy:
- Detecta caras frontales (haarcascade_frontalface_default).
- Si encuentra >=1: coge la mayor, expande con padding (factor del lado de la cara)
y recorta cuadrado.
- Si encuentra 0: fallback a crop cuadrado centrado en el TERCIO SUPERIOR
de la imagen (donde suele estar la cabeza en fotos verticales).
- Redimensiona el cuadrado a `size x size`.
Mantiene aspecto natural — NO estira.
"""
import argparse, os, sys
import cv2
DEFAULT_SIZE = 256
DEFAULT_PADDING = 0.6 # factor del lado de la cara para añadir alrededor
cascade_path = cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
profile_cascade_path = cv2.data.haarcascades + 'haarcascade_profileface.xml'
_face_cascade = cv2.CascadeClassifier(cascade_path)
_profile_cascade = cv2.CascadeClassifier(profile_cascade_path)
def detect_face(gray):
"""Devuelve (x, y, w, h) de la cara más grande, o None."""
for cascade in (_face_cascade, _profile_cascade):
faces = cascade.detectMultiScale(
gray, scaleFactor=1.1, minNeighbors=5, minSize=(30, 30)
)
if len(faces):
# más grande
return max(faces, key=lambda r: r[2] * r[3])
# también lateral en flip
if cascade is _profile_cascade:
flipped = cv2.flip(gray, 1)
faces2 = cascade.detectMultiScale(flipped, 1.1, 5, minSize=(30, 30))
if len(faces2):
x, y, w, h = max(faces2, key=lambda r: r[2] * r[3])
return (gray.shape[1] - x - w, y, w, h)
return None
def square_crop_box(face, img_w, img_h, padding):
"""Caja cuadrada centrada en la cara. Si el padding no cabe sin invadir
lados opuestos (típicamente texto), se REDUCE el side antes que extender.
"""
x, y, w, h = face
cx, cy = x + w / 2, y + h / 2
ideal = max(w, h) * (1 + 2 * padding)
# side máximo manteniendo cara centrada y dentro de la imagen
max_x = 2 * min(cx, img_w - cx)
max_y = 2 * min(cy, img_h - cy)
side = min(ideal, max_x, max_y)
half = side / 2
x1, y1 = int(cx - half), int(cy - half)
x2, y2 = int(cx + half), int(cy + half)
return x1, y1, x2, y2
def fallback_box(img_w, img_h):
"""Sin cara detectada. Heurística por aspect ratio:
- Horizontal (w > h*1.3): cuadrado a la IZQUIERDA (col_* suelen tener foto
a la izquierda y texto a la derecha).
- Vertical o cuadrado: cuadrado anclado al tercio superior, centrado en x.
"""
if img_w > img_h * 1.3:
side = img_h
return 0, 0, side, side
side = min(img_w, img_h)
cx = img_w / 2
x1 = max(0, int(cx - side / 2))
return x1, 0, x1 + side, side
def process(src_path, dst_path, size=DEFAULT_SIZE, padding=DEFAULT_PADDING):
img = cv2.imread(src_path, cv2.IMREAD_UNCHANGED)
if img is None:
return False, 'imread fail'
# convertir alpha → blanco si hace falta
if img.ndim == 3 and img.shape[2] == 4:
# composite sobre blanco
bgr = img[:, :, :3].copy()
alpha = img[:, :, 3] / 255.0
white = (1 - alpha[:, :, None]) * 255
img = (bgr * alpha[:, :, None] + white).astype('uint8')
elif img.ndim == 2:
img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR)
h, w = img.shape[:2]
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
face = detect_face(gray)
if face is not None:
x1, y1, x2, y2 = square_crop_box(face, w, h, padding)
used = 'face'
else:
x1, y1, x2, y2 = fallback_box(w, h)
used = 'fallback'
crop = img[y1:y2, x1:x2]
resized = cv2.resize(crop, (size, size), interpolation=cv2.INTER_AREA)
# asegurar JPEG-safe (sin alpha)
if resized.ndim == 3 and resized.shape[2] == 4:
resized = cv2.cvtColor(resized, cv2.COLOR_BGRA2BGR)
os.makedirs(os.path.dirname(dst_path), exist_ok=True)
cv2.imwrite(dst_path, resized, [cv2.IMWRITE_JPEG_QUALITY, 88])
return True, used
def main():
ap = argparse.ArgumentParser()
ap.add_argument('src')
ap.add_argument('dst')
ap.add_argument('--size', type=int, default=DEFAULT_SIZE)
ap.add_argument('--padding', type=float, default=DEFAULT_PADDING)
ap.add_argument('--batch', action='store_true', help='src y dst son directorios')
args = ap.parse_args()
if args.batch:
files = [f for f in os.listdir(args.src) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
stats = {'face': 0, 'fallback': 0, 'fail': 0}
for fn in files:
ok, info = process(
os.path.join(args.src, fn),
os.path.join(args.dst, os.path.splitext(fn)[0] + '.jpg'),
size=args.size, padding=args.padding,
)
stats['fail' if not ok else info] += 1
print(f'face: {stats["face"]}, fallback: {stats["fallback"]}, fail: {stats["fail"]}')
else:
ok, info = process(args.src, args.dst, size=args.size, padding=args.padding)
print(f'{"OK" if ok else "FAIL"} {info}{args.dst}')
if __name__ == '__main__':
main()