"""Deterministic, neutral pose mannequins for the eleven reference views. These are ordinary RGB image references, not ControlNet or OpenPose maps. They describe pose/camera/framing; a separate character image supplies identity. No network, downloaded models, random sampling, or GPU is used. Pillow is the only third-party dependency. Run ``python pose_guides.py --regenerate`` to rebuild the bundled assets. """ from __future__ import annotations import math from pathlib import Path from PIL import Image, ImageDraw, ImageFont ASSET_DIR = Path(__file__).resolve().parent / "assets" GUIDE_VERSION = 1 # key: (English preview label, pose, camera azimuth, elevation, crop, size) GUIDE_SPECS = { "full_front": ("01 Full / front", "standing", 0, 0, "full", (832, 1216)), "full_left": ("02 Full / left", "standing", 270, 0, "full", (832, 1216)), "full_right": ("03 Full / right", "standing", 90, 0, "full", (832, 1216)), "full_back": ("04 Full / back", "standing", 180, 0, "full", (832, 1216)), "cowboy_front": ("05 Mid-thigh / front", "standing", 0, 0, "cowboy", (1024, 1024)), "cowboy_back": ("06 Mid-thigh / back", "standing", 180, 0, "cowboy", (1024, 1024)), "upper_front": ("07 Upper body / front", "standing", 0, 0, "upper", (1024, 1024)), "face_front": ("08 Face / front", "standing", 0, 0, "face", (1024, 1024)), "knees_front": ("09 Hug knees / front", "knees", 0, 0, "full", (832, 1216)), "knees_back": ("10 Hug knees / back", "knees", 180, 0, "full", (832, 1216)), "prone_back": ("11 Prone / rear, elevated", "prone", 180, 60, "full", (1216, 832)), } def _add(a, b): return tuple(x + y for x, y in zip(a, b)) def _sub(a, b): return tuple(x - y for x, y in zip(a, b)) def _mul(a, scale): return tuple(x * scale for x in a) def _dot(a, b): return sum(x * y for x, y in zip(a, b)) def _cross(a, b): return (a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]) def _unit(a): length = math.sqrt(_dot(a, a)) return _mul(a, 1 / length) if length > 1e-12 else (0.0, 1.0, 0.0) class _Mannequin: """Small ellipsoid mesh model with articulated limbs and subtle joints.""" def __init__(self): self.faces = [] def ellipsoid(self, center, radii, tone=177, axis=(0, 1, 0), facing=None): ey = _unit(axis) if facing is None: helper = (0, 0, 1) if abs(ey[2]) < 0.9 else (0, 1, 0) ex = _unit(_cross(ey, helper)) ez = _unit(_cross(ex, ey)) else: ez = _unit(facing) ex = _unit(_cross(ey, ez)) ez = _unit(_cross(ex, ey)) rings, sectors = 40, 72 vertices = [] for ring in range(rings + 1): latitude = math.pi * ring / rings row = [] for sector in range(sectors): longitude = 2 * math.pi * sector / sectors local = (radii[0] * math.sin(latitude) * math.cos(longitude), radii[1] * math.cos(latitude), radii[2] * math.sin(latitude) * math.sin(longitude)) world = _add(center, _add(_mul(ex, local[0]), _add(_mul(ey, local[1]), _mul(ez, local[2])))) normal = _unit(_add(_mul(ex, local[0] / radii[0] ** 2), _add(_mul(ey, local[1] / radii[1] ** 2), _mul(ez, local[2] / radii[2] ** 2)))) row.append((world, normal)) vertices.append(row) for ring in range(rings): for sector in range(sectors): next_sector = (sector + 1) % sectors corners = [vertices[ring][sector], vertices[ring + 1][sector], vertices[ring + 1][next_sector], vertices[ring][next_sector]] points = tuple(v[0] for v in corners) normal = _unit(tuple(sum(v[1][i] for v in corners) for i in range(3))) self.faces.append((points, normal, tone)) def limb(self, start, end, radius, tone=178): delta = _sub(end, start) length = math.sqrt(_dot(delta, delta)) self.ellipsoid(_mul(_add(start, end), 0.5), (radius, length / 2 + radius * 0.5, radius), tone, delta) def joint(self, point, radius): self.ellipsoid(point, (radius, radius, radius), 160) def head(self, center, axis=(0, 1, 0), facing=(0, 0, 1)): up, forward = _unit(axis), _unit(facing) side = _unit(_cross(up, forward)) self.ellipsoid(center, (0.105, 0.145, 0.101), 187, up, forward) # A short, blunt nose and small ears make front/back/side unambiguous. nose = _add(center, _add(_mul(forward, 0.105), _mul(up, -0.01))) self.ellipsoid(nose, (0.018, 0.029, 0.033), 179, up, forward) for sign in (-1, 1): ear = _add(center, _add(_mul(side, sign * 0.103), _mul(up, -0.005))) self.ellipsoid(ear, (0.019, 0.032, 0.019), 176, up, forward) def _standing(): model = _Mannequin() model.ellipsoid((0, 1.26, 0), (0.189, 0.237, 0.103)) model.ellipsoid((0, 1.045, 0), (0.130, 0.129, 0.091), 172) model.ellipsoid((0, 0.958, -0.002), (0.158, 0.125, 0.108), 180) model.limb((0, 1.438, 0), (0, 1.536, 0), 0.052) model.head((0, 1.665, 0)) for side in (-1, 1): shoulder = (side * 0.196, 1.398, 0) elbow = (side * 0.264, 1.130, 0) wrist = (side * 0.309, 0.901, 0.016) model.joint(shoulder, 0.071) model.limb(shoulder, elbow, 0.063) model.joint(elbow, 0.046) model.limb(elbow, wrist, 0.047) model.joint(wrist, 0.030) model.ellipsoid((side * 0.319, 0.846, 0.02), (0.039, 0.069, 0.025), 183, (side * 0.13, 1, 0)) hip = (side * 0.089, 0.920, 0) knee = (side * 0.105, 0.516, 0.010) ankle = (side * 0.111, 0.097, 0) model.limb(hip, knee, 0.085) model.joint(knee, 0.057) model.limb(knee, ankle, 0.057) model.joint(ankle, 0.039) model.ellipsoid((side * 0.111, 0.049, 0.068), (0.059, 0.048, 0.125), 180) return model def _knees(): model = _Mannequin() model.ellipsoid((0, 0.230, -0.111), (0.163, 0.135, 0.119), 180) model.ellipsoid((0, 0.445, -0.148), (0.151, 0.190, 0.103), 173, (0, 1, 0.10)) model.ellipsoid((0, 0.624, -0.096), (0.183, 0.177, 0.099), 177, (0, 1, 0.24)) model.limb((0, 0.745, -0.065), (0, 0.823, -0.01), 0.050) model.head((0, 0.939, 0.024), axis=(0, 0.99, 0.08), facing=(0, -0.08, 0.99)) for side in (-1, 1): hip = (side * 0.105, 0.226, -0.080) knee = (side * 0.116, 0.603, 0.248) ankle = (side * 0.119, 0.097, 0.320) model.limb(hip, knee, 0.086) model.joint(knee, 0.062) model.limb(knee, ankle, 0.056) model.joint(ankle, 0.038) model.ellipsoid((side * 0.120, 0.050, 0.383), (0.061, 0.050, 0.114), 180) shoulder = (side * 0.186, 0.724, -0.064) elbow = (side * 0.264, 0.465, 0.257) wrist = (side * 0.085, 0.470, 0.356) model.joint(shoulder, 0.063) model.limb(shoulder, elbow, 0.058) model.joint(elbow, 0.043) model.limb(elbow, wrist, 0.043) model.joint(wrist, 0.027) model.ellipsoid((side * 0.037, 0.467 + side * 0.012, 0.359), (0.030, 0.062, 0.023), 184, (1, side * 0.1, 0)) return model def _prone(): model = _Mannequin() model.ellipsoid((0, 0.178, 1.227), (0.189, 0.237, 0.103), axis=(0, 0, 1), facing=(0, -1, 0)) model.ellipsoid((0, 0.165, 1.021), (0.130, 0.130, 0.092), 172, axis=(0, 0, 1), facing=(0, -1, 0)) model.ellipsoid((0, 0.182, 0.924), (0.158, 0.127, 0.111), 180, axis=(0, 0, 1), facing=(0, -1, 0)) model.limb((0, 0.17, 1.418), (0, 0.16, 1.500), 0.052) model.head((0, 0.162, 1.641), axis=(0, 0, 1), facing=(0, -1, 0)) for side in (-1, 1): shoulder = (side * 0.195, 0.172, 1.372) elbow = (side * 0.375, 0.083, 1.606) wrist = (side * 0.244, 0.065, 1.797) model.joint(shoulder, 0.068) model.limb(shoulder, elbow, 0.060) model.joint(elbow, 0.043) model.limb(elbow, wrist, 0.045) model.joint(wrist, 0.027) model.ellipsoid((side * 0.188, 0.052, 1.823), (0.032, 0.066, 0.023), 183, axis=(side, 0, -0.2), facing=(0, -1, 0)) hip = (side * 0.090, 0.177, 0.899) knee = (side * 0.105, 0.116, 0.491) ankle = (side * 0.113, 0.083, 0.098) model.limb(hip, knee, 0.083) model.joint(knee, 0.055) model.limb(knee, ankle, 0.056) model.joint(ankle, 0.036) model.ellipsoid((side * 0.114, 0.058, 0.026), (0.058, 0.111, 0.047), 180, axis=(0, -0.25, 1), facing=(0, -1, -0.25)) return model def _render(model, azimuth, elevation, crop, size, pose): azimuth, elevation = math.radians(azimuth), math.radians(elevation) toward = (math.sin(azimuth) * math.cos(elevation), math.sin(elevation), math.cos(azimuth) * math.cos(elevation)) right = (math.cos(azimuth), 0, -math.sin(azimuth)) up = _cross(toward, right) if pose == "prone": # Roll the overhead camera so the long body fits the landscape bucket. right, up = up, _mul(right, -1) projected = [(_dot(vertex, right), _dot(vertex, up)) for points, _, _ in model.faces for vertex in points] min_x, max_x = min(v[0] for v in projected), max(v[0] for v in projected) min_y, max_y = min(v[1] for v in projected), max(v[1] for v in projected) if crop != "full": # Absolute anatomical cut planes avoid uncertain image segmentation. min_y = {"cowboy": 0.705, "upper": 1.160, "face": 1.493}[crop] max_y += {"cowboy": 0.065, "upper": 0.047, "face": 0.025}[crop] else: height = max_y - min_y min_y -= height * 0.055 max_y += height * 0.065 width = max_x - min_x min_x -= width * 0.075 max_x += width * 0.075 width, height = size scale = min(width / (max_x - min_x), height / (max_y - min_y)) if crop == "full" else height / (max_y - min_y) center_x, center_y = (min_x + max_x) / 2, (min_y + max_y) / 2 supersample = 2 canvas = Image.new("RGB", (width * supersample, height * supersample), "white") draw = ImageDraw.Draw(canvas) # Camera-relative studio lighting makes shape readable at every azimuth. light = _unit(_add(_mul(right, -0.40), _add(_mul(up, 0.68), _mul(toward, 0.80)))) faces = sorted(model.faces, key=lambda face: sum(_dot(p, toward) for p in face[0])) for points, normal, tone in faces: if _dot(normal, toward) < -0.08: continue coords = [(((_dot(p, right) - center_x) * scale + width / 2) * supersample, (height / 2 - (_dot(p, up) - center_y) * scale) * supersample) for p in points] if max(p[1] for p in coords) < 0 or min(p[1] for p in coords) > height * supersample: continue diffuse = max(0, _dot(normal, light)) shade = round(tone * (0.54 + 0.55 * diffuse)) color = (shade, min(255, shade + 2), min(255, shade + 4)) draw.polygon(coords, fill=color) return canvas.resize(size, Image.Resampling.LANCZOS) def _preview(paths): card_w, card_h, padding = 360, 420, 20 sheet = Image.new("RGB", (card_w * 3 + padding * 4, card_h * 4 + padding * 5), "#eef0f3") draw = ImageDraw.Draw(sheet) try: font = ImageFont.truetype("DejaVuSans.ttf", 19) except OSError: font = ImageFont.load_default(size=19) for index, (key, path) in enumerate(paths.items()): x = padding + index % 3 * (card_w + padding) y = padding + index // 3 * (card_h + padding) draw.rounded_rectangle((x, y, x + card_w, y + card_h), radius=12, fill="white") draw.text((x + 16, y + 17), GUIDE_SPECS[key][0], fill="#283444", font=font) with Image.open(path) as source: thumbnail = source.copy() thumbnail.thumbnail((card_w - 24, card_h - 66), Image.Resampling.LANCZOS) sheet.paste(thumbnail, (x + (card_w - thumbnail.width) // 2, y + 52 + (card_h - 66 - thumbnail.height) // 2)) sheet.save(ASSET_DIR / "pose_preview.png", optimize=True) def ensure_guides(*, regenerate: bool = False) -> dict[str, Path]: """Return the eleven bundled PNG paths; create any missing assets locally.""" ASSET_DIR.mkdir(parents=True, exist_ok=True) paths = {key: ASSET_DIR / f"{key}.png" for key in GUIDE_SPECS} models = {} changed = False for key, path in paths.items(): if path.is_file() and not regenerate: continue _, pose, azimuth, elevation, crop, size = GUIDE_SPECS[key] if pose not in models: models[pose] = {"standing": _standing, "knees": _knees, "prone": _prone}[pose]() _render(models[pose], azimuth, elevation, crop, size, pose).save(path, optimize=True) changed = True if changed or not (ASSET_DIR / "pose_preview.png").is_file(): _preview(paths) return paths def get_guide(key: str, width: int, height: int) -> Image.Image: """Load one neutral RGB guide, resizing to the requested output dimensions.""" if key not in GUIDE_SPECS: raise KeyError(f"Unknown pose guide: {key}") if width <= 0 or height <= 0: raise ValueError("Guide width and height must be positive") path = ASSET_DIR / f"{key}.png" if not path.is_file(): path = ensure_guides()[key] with Image.open(path) as source: result = source.convert("RGB") if result.size != (width, height): result = result.resize((width, height), Image.Resampling.LANCZOS) return result if __name__ == "__main__": import argparse parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--regenerate", action="store_true", help="Rebuild all bundled PNGs") args = parser.parse_args() for guide_key, guide_path in ensure_guides(regenerate=args.regenerate).items(): print(f"{guide_key}: {guide_path}") print(f"Preview: {ASSET_DIR / 'pose_preview.png'}")