#!/usr/bin/env python3 """ gen_face.py – Run ONCE to generate assets/face_wire.png ───────────────────────────────────────────────────────── Uses matplotlib's 3-D surface renderer to produce a wireframe face image that display.py loads. Run it once; the PNG is then part of your project. python gen_face.py Dependencies (dev machine only – NOT needed on the target device): sudo apt install python3-matplotlib # or: pip install matplotlib """ import os import math import argparse import numpy as np import matplotlib matplotlib.use('Agg') # off-screen — no display needed import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D # noqa: F401 # ── Face width profile ──────────────────────────────────────────────────────── # (normalised_height, width_fraction) 0 = crown, 1 = chin # Widest point is at the cheekbones (~t=0.37), not the crown. # Crown and forehead are slightly narrower; jaw/chin taper below cheeks. _PROFILE = [ (0.00, 0.82), # crown (0.10, 0.88), # upper skull (0.22, 0.94), # temples / forehead (0.37, 1.00), # cheekbones — widest (0.50, 0.96), # nose level (0.62, 0.84), # mouth level (0.73, 0.68), # jaw angle (0.83, 0.50), # lower jaw (0.92, 0.30), # chin (1.00, 0.08), # chin tip ] def _face_r(v: float) -> float: """Radial width at normalised height v ∈ [0, 1].""" for i in range(len(_PROFILE) - 1): t0, w0 = _PROFILE[i] t1, w1 = _PROFILE[i + 1] if t0 <= v <= t1: return w0 + (v - t0) / (t1 - t0) * (w1 - w0) return 0.0 def face_surface(nu: int = 22, nv: int = 28): """ Build the face mesh arrays. Axis convention chosen to match matplotlib's default camera behaviour: X = left / right Y = depth (positive = toward viewer / front of face) Z = up / down (+1 = crown, −1 = chin) To view from the front: view_init(elev, azim=90) ← camera is at +Y """ u_vals = np.linspace(-math.pi / 2, math.pi / 2, nu) # front hemisphere v_vals = np.linspace(0.0, 1.0, nv) # crown → chin X = np.zeros((nv, nu)) Y = np.zeros((nv, nu)) Z = np.zeros((nv, nu)) for j, v in enumerate(v_vals): r = _face_r(v) z = math.cos(math.pi * v) # +1 at crown, −1 at chin for i, u in enumerate(u_vals): X[j, i] = r * math.sin(u) # left/right Y[j, i] = r * math.cos(u) # depth (front = max) Z[j, i] = z # height return X, Y, Z def generate(out_path: str = 'assets/face_wire.png', img_w: int = 400, img_h: int = 500, line_col: str = '#606060') -> None: dpi = 100 fig = plt.figure(figsize=(img_w / dpi, img_h / dpi), facecolor='black') ax = fig.add_subplot(111, projection='3d', facecolor='black') X, Y, Z = face_surface(nu=18, nv=22) ax.plot_wireframe(X, Y, Z, color=line_col, linewidth=0.85, rstride=1, cstride=1) # azim=90 → camera at +Y, looking toward −Y (directly at face) # elev=-8 → camera slightly below horizontal = view from below, # shows chin/jaw, hides flat top of mesh — matches reference ax.view_init(elev=-8, azim=90) ax.set_axis_off() # [x_width, y_depth, z_height] # Key: y_depth very small → face is flat/mask-like, not a globe ax.set_box_aspect([1.0, 0.28, 1.22]) plt.subplots_adjust(left=0, right=1, top=1, bottom=0) os.makedirs(os.path.dirname(out_path) if os.path.dirname(out_path) else '.', exist_ok=True) fig.savefig(out_path, dpi=dpi, facecolor='black', bbox_inches='tight', pad_inches=0.02) plt.close(fig) print(f'Saved {out_path} ({img_w}×{img_h} px)') if __name__ == '__main__': ap = argparse.ArgumentParser() ap.add_argument('--out', default='assets/face_wire.png') ap.add_argument('--width', type=int, default=400) ap.add_argument('--height', type=int, default=500) ap.add_argument('--color', default='#606060') args = ap.parse_args() generate(args.out, args.width, args.height, args.color)