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:
parent
4e4b27b9e5
commit
0307e048d1
2 changed files with 244 additions and 0 deletions
244
features/video-studio/processing-service/showcase.py
Normal file
244
features/video-studio/processing-service/showcase.py
Normal 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]})")
|
||||
BIN
features/video-studio/processing-service/showcase_output.png
Normal file
BIN
features/video-studio/processing-service/showcase_output.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
Loading…
Add table
Reference in a new issue