Skip to content

API リファレンス

presets

face_fit.presets

Output composition presets.

The numbers follow the photo requirements: - Image size 640 (H) x 480 (W), 4:3. - Face (crown-to-chin) covers about 70-80% of the image height. - White background.

face_ratio defaults to 0.75, the middle of the 0.70-0.80 range. top_margin matches the headroom ratio in the requirement diagram (~9%). eye_line is a reference value for validation/debug (vertical placement is driven by top_margin).

Spec dataclass

Output composition specification.

Attributes:

Name Type Description
out_w int

Output width in pixels.

out_h int

Output height in pixels.

face_ratio float

Fraction of the height the face (crown-to-chin) should occupy.

top_margin float

Fraction of headroom above the crown. Drives vertical placement.

eye_line float

Reference vertical position of the eyes (fraction from top). For validation.

bg tuple[int, int, int]

Background color (RGB) used to fill any margins.

Source code in src/face_fit/presets.py
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@dataclass(frozen=True)
class Spec:
    """Output composition specification.

    Attributes:
        out_w: Output width in pixels.
        out_h: Output height in pixels.
        face_ratio: Fraction of the height the face (crown-to-chin) should occupy.
        top_margin: Fraction of headroom above the crown. Drives vertical placement.
        eye_line: Reference vertical position of the eyes (fraction from top). For validation.
        bg: Background color (RGB) used to fill any margins.
    """

    out_w: int = 480
    out_h: int = 640
    face_ratio: float = 0.75
    top_margin: float = 0.09
    eye_line: float = 0.45
    bg: tuple[int, int, int] = (255, 255, 255)

get_preset(name)

Return a preset by name. Raises KeyError for an unknown name.

Source code in src/face_fit/presets.py
48
49
50
51
52
53
def get_preset(name: str) -> Spec:
    """Return a preset by name. Raises ``KeyError`` for an unknown name."""
    try:
        return PRESETS[name]
    except KeyError as exc:  # pragma: no cover - handled by the CLI
        raise KeyError(f"unknown preset: {name!r} (available: {sorted(PRESETS)})") from exc

core

face_fit.core

Shared high-level pipeline used by the CLI commands.

Wraps the load -> detect -> fit -> save flow and small helpers (output naming, input expansion, opening files) so that the fit and batch commands stay thin. Reuses :mod:face_fit.landmarks, :mod:face_fit.compose, :mod:face_fit.render and :mod:face_fit.presets.

FitReport dataclass

Outcome of fitting one image.

Source code in src/face_fit/core.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@dataclass(frozen=True)
class FitReport:
    """Outcome of fitting one image."""

    input: Path
    output: Path
    width: int
    height: int
    face_ratio: float
    face_ratio_ok: bool
    eye_line: float
    roll_deg: float
    crown_source: str
    scale: float
    debug_path: Path | None

    def to_dict(self) -> dict:
        """Return a JSON-serializable summary."""
        return {
            "input": str(self.input),
            "output": str(self.output),
            "size": [self.width, self.height],
            "face_ratio": round(self.face_ratio, 4),
            "face_ratio_ok": self.face_ratio_ok,
            "eye_line": round(self.eye_line, 4),
            "roll_deg": round(self.roll_deg, 3),
            "crown_source": self.crown_source,
            "scale": round(self.scale, 4),
            "debug_path": str(self.debug_path) if self.debug_path else None,
        }

to_dict()

Return a JSON-serializable summary.

Source code in src/face_fit/core.py
47
48
49
50
51
52
53
54
55
56
57
58
59
60
def to_dict(self) -> dict:
    """Return a JSON-serializable summary."""
    return {
        "input": str(self.input),
        "output": str(self.output),
        "size": [self.width, self.height],
        "face_ratio": round(self.face_ratio, 4),
        "face_ratio_ok": self.face_ratio_ok,
        "eye_line": round(self.eye_line, 4),
        "roll_deg": round(self.roll_deg, 3),
        "crown_source": self.crown_source,
        "scale": round(self.scale, 4),
        "debug_path": str(self.debug_path) if self.debug_path else None,
    }

build_spec(preset, *, width=None, height=None, face_ratio=None, top_margin=None)

Resolve a preset and apply optional overrides.

Source code in src/face_fit/core.py
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
def build_spec(
    preset: str,
    *,
    width: int | None = None,
    height: int | None = None,
    face_ratio: float | None = None,
    top_margin: float | None = None,
) -> Spec:
    """Resolve a preset and apply optional overrides."""
    spec = get_preset(preset)
    overrides: dict[str, object] = {}
    if width:
        overrides["out_w"] = width
    if height:
        overrides["out_h"] = height
    if face_ratio is not None:
        overrides["face_ratio"] = face_ratio
    if top_margin is not None:
        overrides["top_margin"] = top_margin
    return dataclasses.replace(spec, **overrides) if overrides else spec

default_output(in_path)

Derive a default output path next to the input (<stem>_fitted.jpg).

Source code in src/face_fit/core.py
85
86
87
def default_output(in_path: Path) -> Path:
    """Derive a default output path next to the input (``<stem>_fitted.jpg``)."""
    return in_path.with_name(in_path.stem + "_fitted.jpg")

fit_file(in_path, out_path, spec, *, render_scale=2, quality=95, debug=False, detector=None)

Run the full pipeline on one file and write the JPEG output.

Parameters:

Name Type Description Default
in_path Path

Source image path.

required
out_path Path

Destination JPEG path.

required
spec Spec

Target composition.

required
render_scale int

Internal supersampling factor.

2
quality int

JPEG quality.

95
debug bool

Also write a *_debug.png with composition guides.

False
detector FaceDetector | None

Reusable detector (recommended for batch); falls back to the shared default detector when None.

None

Returns:

Name Type Description
A FitReport

class:FitReport.

Source code in src/face_fit/core.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
def fit_file(
    in_path: Path,
    out_path: Path,
    spec: Spec,
    *,
    render_scale: int = 2,
    quality: int = 95,
    debug: bool = False,
    detector: FaceDetector | None = None,
) -> FitReport:
    """Run the full pipeline on one file and write the JPEG output.

    Args:
        in_path: Source image path.
        out_path: Destination JPEG path.
        spec: Target composition.
        render_scale: Internal supersampling factor.
        quality: JPEG quality.
        debug: Also write a ``*_debug.png`` with composition guides.
        detector: Reusable detector (recommended for batch); falls back to the
            shared default detector when ``None``.

    Returns:
        A :class:`FitReport`.
    """
    rgb = load_rgb(in_path)
    geom = detector.detect(rgb) if detector is not None else detect_face(rgb)
    out_img, fit = fit_to_image(rgb, geom, spec, render_scale=render_scale)
    out_path.parent.mkdir(parents=True, exist_ok=True)
    save_jpeg(out_img, out_path, quality=quality)

    debug_path: Path | None = None
    if debug:
        debug_img = draw_debug(out_img, spec, fit)
        debug_path = out_path.with_name(out_path.stem + "_debug.png")
        save_png(debug_img, debug_path)

    face_pct = fit.chin_line_actual - spec.top_margin
    return FitReport(
        input=in_path,
        output=out_path,
        width=spec.out_w,
        height=spec.out_h,
        face_ratio=face_pct,
        face_ratio_ok=FACE_RATIO_MIN <= face_pct <= FACE_RATIO_MAX,
        eye_line=fit.eye_line_actual,
        roll_deg=fit.roll_deg,
        crown_source="segmentation" if geom.crown_from_segmentation else "extrapolated",
        scale=fit.scale,
        debug_path=debug_path,
    )

iter_image_files(inputs)

Expand a list of files, directories and glob patterns into image paths.

Directories are scanned non-recursively. Results are de-duplicated and sorted.

Source code in src/face_fit/core.py
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
def iter_image_files(inputs: list[str]) -> list[Path]:
    """Expand a list of files, directories and glob patterns into image paths.

    Directories are scanned non-recursively. Results are de-duplicated and sorted.
    """
    found: list[Path] = []
    seen: set[Path] = set()

    def add(p: Path) -> None:
        rp = p.resolve()
        if rp not in seen and p.suffix.lower() in IMAGE_EXTS:
            seen.add(rp)
            found.append(p)

    for item in inputs:
        path = Path(item)
        if path.is_dir():
            for child in sorted(path.iterdir()):
                if child.is_file():
                    add(child)
        elif any(ch in item for ch in "*?[") and not path.exists():
            for match in sorted(glob.glob(item)):  # noqa: PTH207 (glob patterns)
                mp = Path(match)
                if mp.is_file():
                    add(mp)
        elif path.is_file():
            add(path)
    return found

open_file(path)

Open a file with the OS default application (Windows/macOS/Linux).

Source code in src/face_fit/core.py
173
174
175
176
177
178
179
180
181
def open_file(path: Path) -> None:
    """Open a file with the OS default application (Windows/macOS/Linux)."""
    target = str(path)
    startfile = getattr(os, "startfile", None)
    if startfile is not None:  # Windows
        startfile(target)
        return
    opener = "open" if sys.platform == "darwin" else "xdg-open"
    subprocess.run([opener, target], check=False)  # noqa: S603

compose

face_fit.compose

Composition math (pure functions).

Given the face reference points (crown, chin, both eyes) and a target composition :class:~face_fit.presets.Spec, compute a similarity transform (roll correction + uniform scale + translation).

Coordinates are image pixels (x right, y down). The only dependency is numpy and nothing touches the image itself, which keeps this easy to unit-test.

Fit dataclass

Result of the similarity-transform computation.

Attributes:

Name Type Description
forward ndarray

2x3 affine matrix mapping source -> output.

inverse_coeffs tuple[float, float, float, float, float, float]

Coefficients for PIL Image.transform(AFFINE) (output -> source) as (a, b, c, d, e, f).

scale float

Applied uniform scale.

roll_deg float

Corrected roll angle, in degrees.

eye_line_actual float

Eye line in the output, as a fraction of the height.

chin_line_actual float

Chin line in the output, as a fraction of the height.

info_points dict[str, Point]

Reference points mapped to output coordinates, for debugging.

Source code in src/face_fit/compose.py
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@dataclass(frozen=True)
class Fit:
    """Result of the similarity-transform computation.

    Attributes:
        forward: 2x3 affine matrix mapping source -> output.
        inverse_coeffs: Coefficients for PIL ``Image.transform(AFFINE)``
            (output -> source) as ``(a, b, c, d, e, f)``.
        scale: Applied uniform scale.
        roll_deg: Corrected roll angle, in degrees.
        eye_line_actual: Eye line in the output, as a fraction of the height.
        chin_line_actual: Chin line in the output, as a fraction of the height.
        info_points: Reference points mapped to output coordinates, for debugging.
    """

    forward: np.ndarray
    inverse_coeffs: tuple[float, float, float, float, float, float]
    scale: float
    roll_deg: float
    eye_line_actual: float
    chin_line_actual: float
    info_points: dict[str, Point]

compute_fit(*, crown, chin, eye_left, eye_right, spec)

Compute the similarity transform from reference points and a spec.

Parameters:

Name Type Description Default
crown Point

Top of the head (source px).

required
chin Point

Chin tip (source px).

required
eye_left Point

Center of the left-in-image eye (smaller x).

required
eye_right Point

Center of the right-in-image eye.

required
spec Spec

Target composition.

required

Returns:

Name Type Description
A Fit

class:Fit.

Constraints
  • Level the line between the eyes (roll correction).
  • Scale so crown-to-chin equals spec.face_ratio * out_h.
  • Place the crown at y = top_margin * out_h (vertical).
  • Place the eye midpoint at x = out_w / 2 (horizontal).
Source code in src/face_fit/compose.py
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def compute_fit(
    *,
    crown: Point,
    chin: Point,
    eye_left: Point,
    eye_right: Point,
    spec: Spec,
) -> Fit:
    """Compute the similarity transform from reference points and a spec.

    Args:
        crown: Top of the head (source px).
        chin: Chin tip (source px).
        eye_left: Center of the left-in-image eye (smaller x).
        eye_right: Center of the right-in-image eye.
        spec: Target composition.

    Returns:
        A :class:`Fit`.

    Constraints:
        - Level the line between the eyes (roll correction).
        - Scale so crown-to-chin equals ``spec.face_ratio * out_h``.
        - Place the crown at ``y = top_margin * out_h`` (vertical).
        - Place the eye midpoint at ``x = out_w / 2`` (horizontal).
    """
    # Normalize so the smaller-x point is "left" (robust to swapped arguments).
    if eye_left[0] > eye_right[0]:
        eye_left, eye_right = eye_right, eye_left

    p_left = np.array(eye_left, dtype=float)
    p_right = np.array(eye_right, dtype=float)
    eye_mid = (p_left + p_right) / 2.0

    # Roll angle theta: tilt of the eye line. Rotate by phi = -theta to level it.
    theta = math.atan2(p_right[1] - p_left[1], p_right[0] - p_left[0])
    cos_t = math.cos(theta)
    sin_t = math.sin(theta)

    # Components of rotation R(phi = -theta).
    r00, r01 = cos_t, sin_t
    r10, r11 = -sin_t, cos_t

    # Measure face height along the corrected "down" axis (removes the roll effect).
    down = np.array([-sin_t, cos_t])  # output-down direction in source coords
    crown_v = np.array(crown, dtype=float)
    chin_v = np.array(chin, dtype=float)
    face_height = float((chin_v - crown_v) @ down)
    if face_height <= _MIN_FACE_HEIGHT:
        raise ValueError(
            f"non-positive face height (crown below chin?): face_height={face_height:.3f}"
        )

    scale = spec.face_ratio * spec.out_h / face_height

    # Translation: horizontal = eye midpoint to center, vertical = crown to top_margin.
    tx = spec.out_w / 2.0 - scale * (r00 * eye_mid[0] + r01 * eye_mid[1])
    ty = spec.top_margin * spec.out_h - scale * (r10 * crown_v[0] + r11 * crown_v[1])

    forward = np.array(
        [
            [scale * r00, scale * r01, tx],
            [scale * r10, scale * r11, ty],
        ],
        dtype=float,
    )

    # Invert to output -> source (for PIL).
    m3 = np.vstack([forward, [0.0, 0.0, 1.0]])
    inv = np.linalg.inv(m3)
    inverse_coeffs = (
        float(inv[0, 0]),
        float(inv[0, 1]),
        float(inv[0, 2]),
        float(inv[1, 0]),
        float(inv[1, 1]),
        float(inv[1, 2]),
    )

    out_crown = _affine(forward, crown)
    out_chin = _affine(forward, chin)
    out_eye = _affine(forward, tuple(eye_mid))
    out_eye_l = _affine(forward, tuple(p_left))
    out_eye_r = _affine(forward, tuple(p_right))

    return Fit(
        forward=forward,
        inverse_coeffs=inverse_coeffs,
        scale=scale,
        roll_deg=math.degrees(theta),
        eye_line_actual=out_eye[1] / spec.out_h,
        chin_line_actual=out_chin[1] / spec.out_h,
        info_points={
            "crown": out_crown,
            "chin": out_chin,
            "eye_mid": out_eye,
            "eye_left": out_eye_l,
            "eye_right": out_eye_r,
        },
    )

landmarks

face_fit.landmarks

Face landmark detection + crown estimation via white-background segmentation.

MediaPipe FaceLandmarker (Tasks API, 478 landmarks) provides the eyes, chin, face width and center. The crown (top of the head), which is not a landmark, is recovered by extracting the subject silhouette from the white background. Image I/O uses PIL (Unicode-path safe); array work uses numpy/OpenCV.

Use :class:FaceDetector to load the model once and process many images (e.g. in batch mode); :func:detect_face is a convenience wrapper over a shared default detector.

FaceDetector

Reusable face detector that loads the MediaPipe model once.

Create one instance and call :meth:detect for many images (the heavy model load happens only once). Usable as a context manager.

Source code in src/face_fit/landmarks.py
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
class FaceDetector:
    """Reusable face detector that loads the MediaPipe model once.

    Create one instance and call :meth:`detect` for many images (the heavy model
    load happens only once). Usable as a context manager.
    """

    def __init__(self) -> None:
        """Create a detector; the model loads lazily on first use."""
        self._landmarker = None

    def _landmarker_obj(self):  # pragma: no cover - mediapipe runtime
        if self._landmarker is None:
            base = mp_python.BaseOptions(model_asset_path=str(ensure_model()))
            options = mp_vision.FaceLandmarkerOptions(base_options=base, num_faces=5)
            self._landmarker = mp_vision.FaceLandmarker.create_from_options(options)
        return self._landmarker

    def _run(self, rgb: np.ndarray):  # pragma: no cover - mediapipe runtime (integration-tested)
        """Return the largest face's landmark list, or ``None`` if no face is found."""
        mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb)
        result = self._landmarker_obj().detect(mp_image)
        if not result.face_landmarks:
            return None
        return max(result.face_landmarks, key=lambda lms: abs(lms[_L_IRIS].x - lms[_R_IRIS].x))

    def detect(self, rgb: np.ndarray) -> FaceGeometry:
        """Extract face geometry from an RGB array. Raises ``RuntimeError`` if no face."""
        lms = self._run(rgb)
        if lms is None:
            raise RuntimeError("no face detected")
        return _geometry_from_landmarks(rgb, lms)

    def close(self) -> None:
        """Release the underlying MediaPipe landmarker."""
        if self._landmarker is not None:  # pragma: no cover - mediapipe runtime
            self._landmarker.close()
            self._landmarker = None

    def __enter__(self) -> FaceDetector:
        """Enter the context, returning this detector."""
        return self

    def __exit__(self, *exc) -> None:
        """Exit the context, releasing the landmarker."""
        self.close()

__enter__()

Enter the context, returning this detector.

Source code in src/face_fit/landmarks.py
193
194
195
def __enter__(self) -> FaceDetector:
    """Enter the context, returning this detector."""
    return self

__exit__(*exc)

Exit the context, releasing the landmarker.

Source code in src/face_fit/landmarks.py
197
198
199
def __exit__(self, *exc) -> None:
    """Exit the context, releasing the landmarker."""
    self.close()

__init__()

Create a detector; the model loads lazily on first use.

Source code in src/face_fit/landmarks.py
161
162
163
def __init__(self) -> None:
    """Create a detector; the model loads lazily on first use."""
    self._landmarker = None

close()

Release the underlying MediaPipe landmarker.

Source code in src/face_fit/landmarks.py
187
188
189
190
191
def close(self) -> None:
    """Release the underlying MediaPipe landmarker."""
    if self._landmarker is not None:  # pragma: no cover - mediapipe runtime
        self._landmarker.close()
        self._landmarker = None

detect(rgb)

Extract face geometry from an RGB array. Raises RuntimeError if no face.

Source code in src/face_fit/landmarks.py
180
181
182
183
184
185
def detect(self, rgb: np.ndarray) -> FaceGeometry:
    """Extract face geometry from an RGB array. Raises ``RuntimeError`` if no face."""
    lms = self._run(rgb)
    if lms is None:
        raise RuntimeError("no face detected")
    return _geometry_from_landmarks(rgb, lms)

FaceGeometry dataclass

Detected face geometry (all values in source px).

Source code in src/face_fit/landmarks.py
45
46
47
48
49
50
51
52
53
54
@dataclass(frozen=True)
class FaceGeometry:
    """Detected face geometry (all values in source px)."""

    crown: Point
    chin: Point
    eye_left: Point  # image-left (smaller x)
    eye_right: Point
    face_width: float
    crown_from_segmentation: bool

detect_face(rgb)

Detect face geometry using a shared default :class:FaceDetector.

Source code in src/face_fit/landmarks.py
207
208
209
def detect_face(rgb: np.ndarray) -> FaceGeometry:
    """Detect face geometry using a shared default :class:`FaceDetector`."""
    return _shared_detector().detect(rgb)

load_rgb(path)

Load an image as a uint8 RGB array (applies EXIF orientation; Unicode-safe).

Source code in src/face_fit/landmarks.py
57
58
59
60
61
62
def load_rgb(path: str | Path) -> np.ndarray:
    """Load an image as a uint8 RGB array (applies EXIF orientation; Unicode-safe)."""
    data = Path(path).read_bytes()
    img = Image.open(io.BytesIO(data))
    img = ImageOps.exif_transpose(img).convert("RGB")
    return np.ascontiguousarray(np.asarray(img, dtype=np.uint8))

render

face_fit.render

Apply the similarity transform, fill the white background, downscale, and save (Pillow).

draw_debug(out_img, spec, fit)

Return a debug image with composition guides (crown/chin/center/eye lines) and points.

Source code in src/face_fit/render.py
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
def draw_debug(out_img: Image.Image, spec: Spec, fit: Fit) -> Image.Image:
    """Return a debug image with composition guides (crown/chin/center/eye lines) and points."""
    img = out_img.copy().convert("RGB")
    draw = ImageDraw.Draw(img)
    w, h = spec.out_w, spec.out_h

    crown_y = spec.top_margin * h
    chin_y = (spec.top_margin + spec.face_ratio) * h
    draw.line([(0, crown_y), (w, crown_y)], fill=(0, 170, 255), width=1)
    draw.line([(0, chin_y), (w, chin_y)], fill=(0, 170, 255), width=1)
    draw.line([(w / 2, 0), (w / 2, h)], fill=(0, 255, 0), width=1)
    eye_y = fit.eye_line_actual * h
    draw.line([(0, eye_y), (w, eye_y)], fill=(255, 120, 0), width=1)

    for name, (px, py) in fit.info_points.items():
        r = 3
        color = (255, 0, 0) if name in ("crown", "chin") else (255, 0, 255)
        draw.ellipse([px - r, py - r, px + r, py + r], outline=color, width=2)
    return img

fit_to_image(rgb, geom, spec, render_scale=2)

Produce the output image from the geometry and spec.

The affine transform is applied onto a render_scale-times larger canvas at full resolution, then downscaled with LANCZOS to reduce aliasing. Margins and missing areas are filled with the background color. No retouching (color/skin) is performed.

Returns:

Type Description
tuple[Image, Fit]

(output image, Fit at the final size).

Source code in src/face_fit/render.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def fit_to_image(
    rgb: np.ndarray, geom: FaceGeometry, spec: Spec, render_scale: int = 2
) -> tuple[Image.Image, Fit]:
    """Produce the output image from the geometry and spec.

    The affine transform is applied onto a ``render_scale``-times larger canvas
    at full resolution, then downscaled with LANCZOS to reduce aliasing. Margins
    and missing areas are filled with the background color. No retouching
    (color/skin) is performed.

    Returns:
        ``(output image, Fit at the final size)``.
    """
    big = dataclasses.replace(
        spec, out_w=spec.out_w * render_scale, out_h=spec.out_h * render_scale
    )
    fit_big = compute_fit(
        crown=geom.crown,
        chin=geom.chin,
        eye_left=geom.eye_left,
        eye_right=geom.eye_right,
        spec=big,
    )

    src = Image.fromarray(rgb)
    big_img = src.transform(
        (big.out_w, big.out_h),
        Image.Transform.AFFINE,
        fit_big.inverse_coeffs,
        resample=Image.Resampling.BICUBIC,
        fillcolor=spec.bg,
    )
    out = big_img.resize((spec.out_w, spec.out_h), Image.Resampling.LANCZOS)

    fit_final = compute_fit(
        crown=geom.crown,
        chin=geom.chin,
        eye_left=geom.eye_left,
        eye_right=geom.eye_right,
        spec=spec,
    )
    return out, fit_final

save_jpeg(img, path, quality=95)

Save as JPEG (Unicode-safe; chroma subsampling disabled for quality).

Source code in src/face_fit/render.py
82
83
84
85
86
def save_jpeg(img: Image.Image, path: str | Path, quality: int = 95) -> None:
    """Save as JPEG (Unicode-safe; chroma subsampling disabled for quality)."""
    buf = io.BytesIO()
    img.convert("RGB").save(buf, format="JPEG", quality=quality, subsampling=0)
    Path(path).write_bytes(buf.getvalue())

save_png(img, path)

Save as PNG (for debug images; Unicode-safe).

Source code in src/face_fit/render.py
89
90
91
92
93
def save_png(img: Image.Image, path: str | Path) -> None:
    """Save as PNG (for debug images; Unicode-safe)."""
    buf = io.BytesIO()
    img.convert("RGB").save(buf, format="PNG")
    Path(path).write_bytes(buf.getvalue())

model

face_fit.model

Fetch and cache the MediaPipe FaceLandmarker model (.task).

ensure_model()

Return the model path, downloading and caching it if missing.

Source code in src/face_fit/model.py
23
24
25
26
27
28
29
30
31
32
33
34
35
36
def ensure_model() -> Path:
    """Return the model path, downloading and caching it if missing."""
    cache = _cache_dir()
    cache.mkdir(parents=True, exist_ok=True)
    path = cache / _MODEL_NAME
    if path.exists() and path.stat().st_size > 0:
        return path

    tmp = path.with_suffix(".task.part")
    with urllib.request.urlopen(_MODEL_URL) as resp:  # noqa: S310 (trusted Google CDN)
        data = resp.read()
    tmp.write_bytes(data)
    tmp.replace(path)
    return path