feat(processing-service): Add showcase generation logic to produce visual outputs like thumbnails in the video processing pipeline

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-17 21:26:09 -07:00
parent 4e4b27b9e5
commit 0307e048d1
2 changed files with 244 additions and 0 deletions

View file

@ -0,0 +1,244 @@
#!/usr/bin/env python3
"""Visual showcase of GimpMaskModifier on different face shapes.
Generates synthetic face geometry (mimicking InsightFace buffalo_l output)
for six face archetypes, applies the procedural mask, and saves a grid.
Usage: python showcase.py
Output: showcase_output.png
"""
from __future__ import annotations
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "src"))
import math
import cv2
import numpy as np
from modifiers.mask import GimpMaskModifier
from models.types import FaceRegion
# ---------------------------------------------------------------------------
# Synthetic face generator
# ---------------------------------------------------------------------------
def make_face_oval_landmarks(
cx: float, cy: float,
rx: float, ry: float,
angle_deg: float = 0.0,
n: int = 106,
noise: float = 3.0,
seed: int = 0,
) -> list[tuple[float, float]]:
"""Generate 106 landmark points on a rotated + noisy ellipse."""
rng = np.random.default_rng(seed)
angles = np.linspace(0, 2 * math.pi, n, endpoint=False)
a_rad = math.radians(angle_deg)
cos_a, sin_a = math.cos(a_rad), math.sin(a_rad)
pts = []
for theta in angles:
lx = rx * math.cos(theta) + rng.normal(0, noise)
ly = ry * math.sin(theta) + rng.normal(0, noise)
x = cx + cos_a * lx - sin_a * ly
y = cy + sin_a * lx + cos_a * ly
pts.append((float(x), float(y)))
return pts
def make_kps(cx, cy, rx, ry, angle_deg=0.0):
"""Synthesise 5 InsightFace keypoints from face parameters."""
a = math.radians(angle_deg)
eye_y_off = -ry * 0.20
mouth_y_off = ry * 0.38
mouth_x_off = rx * 0.32
def rot(dx, dy):
return (cx + math.cos(a)*dx - math.sin(a)*dy,
cy + math.sin(a)*dx + math.cos(a)*dy)
le = rot(-rx * 0.32, eye_y_off)
re = rot( rx * 0.32, eye_y_off)
nos = rot(0, -ry * 0.02)
lm = rot(-mouth_x_off, mouth_y_off)
rm = rot( mouth_x_off, mouth_y_off)
return [le, re, nos, lm, rm]
def make_bbox(cx, cy, rx, ry, angle_deg=0.0, pad=1.05):
x1 = int(cx - rx * pad)
y1 = int(cy - ry * pad)
x2 = int(cx + rx * pad)
y2 = int(cy + ry * pad)
return (x1, y1, x2, y2)
def skin_background(h: int, w: int, seed: int = 42) -> np.ndarray:
"""A realistic skin-tone gradient background for each portrait."""
rng = np.random.default_rng(seed)
base_b = int(rng.integers(120, 160))
base_g = int(rng.integers(140, 190))
base_r = int(rng.integers(160, 210))
img = np.zeros((h, w, 3), dtype=np.float32)
for y in range(h):
t = y / h
img[y, :] = (
base_b * (1 - t * 0.15),
base_g * (1 - t * 0.10),
base_r * (1 - t * 0.08),
)
# Add some skin texture noise
noise = rng.normal(0, 6, (h, w, 3))
img = np.clip(img + noise, 0, 255).astype(np.uint8)
return img
# ---------------------------------------------------------------------------
# Face archetypes
# ---------------------------------------------------------------------------
ARCHETYPES = [
{
"label": "Round",
"rx": 100, "ry": 100,
"angle": 0.0,
"desc": "Equal width/height",
"seed": 1,
},
{
"label": "Long & Narrow",
"rx": 70, "ry": 130,
"angle": 0.0,
"desc": "Tall face, narrow jaw",
"seed": 2,
},
{
"label": "Wide & Short",
"rx": 130, "ry": 80,
"angle": 0.0,
"desc": "Broad face, short height",
"seed": 3,
},
{
"label": "Tilted Left",
"rx": 95, "ry": 115,
"angle": -18.0,
"desc": "18° head tilt",
"seed": 4,
},
{
"label": "Tilted Right",
"rx": 95, "ry": 115,
"angle": 18.0,
"desc": "18° head tilt",
"seed": 5,
},
{
"label": "Oval (Classic)",
"rx": 85, "ry": 115,
"angle": 0.0,
"desc": "Standard oval proportion",
"seed": 6,
},
]
CELL_W = 340
LABEL_H = 54 # label bar below each frame
CELL_H = 420 # frame-only height
COLS = 3
ROWS = math.ceil(len(ARCHETYPES) / COLS)
PAD = 20
HEADER_H = 60
FOOTER_H = 50
SLOT_H = CELL_H + LABEL_H # full height of one grid slot
TOTAL_W = COLS * CELL_W + (COLS + 1) * PAD
TOTAL_H = ROWS * SLOT_H + (ROWS + 1) * PAD + HEADER_H + FOOTER_H
FONT = cv2.FONT_HERSHEY_DUPLEX
FONT_SM = cv2.FONT_HERSHEY_SIMPLEX
GOLD = (40, 180, 230)
WHITE = (255, 255, 255)
DARK = (30, 30, 30)
BG_CANVAS = (20, 18, 18)
def draw_cell(archetype: dict, mask: GimpMaskModifier) -> np.ndarray:
cx, cy = CELL_W // 2, CELL_H // 2 - 20
rx, ry = archetype["rx"], archetype["ry"]
angle = archetype["angle"]
seed = archetype["seed"]
frame = skin_background(CELL_H, CELL_W, seed=seed)
kps = make_kps(cx, cy, rx, ry, angle)
lm106 = make_face_oval_landmarks(cx, cy, rx, ry, angle, noise=2.5, seed=seed)
bbox = make_bbox(cx, cy, rx, ry, angle)
face = FaceRegion(bbox=bbox, confidence=0.97, keypoints=kps, landmarks_106=lm106)
frame = mask.apply(frame, face)
# Label bar at bottom
bar_h = 54
bar = np.full((bar_h, CELL_W, 3), (12, 10, 10), dtype=np.uint8)
cv2.putText(bar, archetype["label"], (10, 26),
FONT, 0.65, GOLD, 1, cv2.LINE_AA)
cv2.putText(bar, archetype["desc"], (10, 46),
FONT_SM, 0.42, (180, 180, 180), 1, cv2.LINE_AA)
return np.vstack([frame, bar])
def render_grid(mask: GimpMaskModifier) -> np.ndarray:
canvas = np.full((TOTAL_H, TOTAL_W, 3), BG_CANVAS, dtype=np.uint8)
# Header
cv2.putText(canvas, "GimpMaskModifier — Procedural Face-Shape Adaptation",
(PAD, 42), FONT, 0.70, GOLD, 1, cv2.LINE_AA)
cv2.line(canvas, (PAD, HEADER_H - 6), (TOTAL_W - PAD, HEADER_H - 6),
(60, 55, 55), 1)
for i, arch in enumerate(ARCHETYPES):
col = i % COLS
row = i // COLS
x0 = PAD + col * (CELL_W + PAD)
y0 = HEADER_H + PAD + row * (SLOT_H + PAD)
cell = draw_cell(arch, mask)
canvas[y0: y0 + cell.shape[0], x0: x0 + cell.shape[1]] = cell
# Cell border
ch, cw = cell.shape[:2]
cv2.rectangle(canvas, (x0 - 1, y0 - 1), (x0 + cw, y0 + ch), (70, 60, 60), 1)
# Footer
fy = TOTAL_H - FOOTER_H + 24
cv2.putText(canvas,
"Shape source: InsightFace 106-pt convex hull | "
"Eyes: landmark-proximity cluster | "
"Mouth: 5-kps",
(PAD, fy), FONT_SM, 0.38, (120, 120, 120), 1, cv2.LINE_AA)
return canvas
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
if __name__ == "__main__":
mask = GimpMaskModifier()
grid = render_grid(mask)
out_path = Path(__file__).parent / "showcase_output.png"
cv2.imwrite(str(out_path), grid)
print(f"Saved: {out_path} ({grid.shape[1]}×{grid.shape[0]})")

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB