Source code for facemask.masking

"""
Pure geometry/pixel helpers for face anonymization — kept dependency-free
(only numpy/opencv) and side-effect-free so they're cheap to unit test.
"""

from __future__ import annotations

import cv2
import numpy as np

Box = tuple[float, float, float, float]  # x1, y1, x2, y2 (pixel coords)


[docs] def expand_and_clip(box: Box, margin_frac: float, frame_w: int, frame_h: int) -> Box: """Pad a face box and clip it to the frame bounds. Pads ``box`` by ``margin_frac`` of its own width/height (so hairline, ears, and chin are covered, not just the tight landmark/detector box), then clips the result to ``[0, frame_w] x [0, frame_h]``. Parameters ---------- box : Box ``(x1, y1, x2, y2)`` pixel coordinates of the detected face box. margin_frac : float Fraction of the box's own width/height to pad on each side (e.g. ``0.25`` pads a 100 px-wide box by 25 px per side). frame_w, frame_h : int Frame dimensions in pixels, used as the clip bounds. Returns ------- Box The padded, clipped ``(x1, y1, x2, y2)`` box. Always a valid, non-negative box even when the input box touches the frame edge. Notes ----- Padding is applied before clipping, so a face near the frame edge is padded first and only then clamped — it cannot produce a negative or out-of-bounds coordinate a caller could mis-slice. See :doc:`/math/face_masking` for the exact padding/clamp formula. """ x1, y1, x2, y2 = box w = max(0.0, x2 - x1) h = max(0.0, y2 - y1) mx = w * margin_frac my = h * margin_frac x1 = max(0.0, x1 - mx) y1 = max(0.0, y1 - my) x2 = min(float(frame_w), x2 + mx) y2 = min(float(frame_h), y2 + my) return (x1, y1, x2, y2)
[docs] def apply_mask(frame: np.ndarray, boxes: list[Box], style: str) -> np.ndarray: """Blur or solid-fill every box's region in ``frame``, in place. Parameters ---------- frame : numpy.ndarray BGR frame, modified in place. boxes : list of Box Regions to mask, e.g. from :func:`expand_and_clip`. A degenerate box (``x2<=x1`` or ``y2<=y1``) is silently skipped. style : {"blur", "box"} ``"blur"`` applies a Gaussian blur with its kernel size scaled to the box's own size; ``"box"`` fills the region with a solid color. Returns ------- numpy.ndarray ``frame``, returned for convenient call-site chaining (it was already modified in place). Notes ----- See :doc:`/math/face_masking` for the blur-kernel sizing formula. """ for x1, y1, x2, y2 in boxes: ix1, iy1 = int(round(x1)), int(round(y1)) ix2, iy2 = int(round(x2)), int(round(y2)) if ix2 <= ix1 or iy2 <= iy1: continue if style == "box": cv2.rectangle(frame, (ix1, iy1), (ix2, iy2), (20, 20, 20), thickness=-1) continue region_w, region_h = ix2 - ix1, iy2 - iy1 # A region clipped to a thin sliver near the frame edge (a real, # observed case — expand_and_clip() clamps to frame bounds, so an # edge-adjacent detection can legitimately produce one) can be # smaller than a size-proportional kernel would need — cv2.GaussianBlur # throws if ksize exceeds the region's own dimensions. Fall back to # the same solid fill as the "box" style rather than risk leaving a # face unmasked or crashing the whole run. if min(region_w, region_h) < 3: cv2.rectangle(frame, (ix1, iy1), (ix2, iy2), (20, 20, 20), thickness=-1) continue def _largest_odd_leq(n: int) -> int: return n if n % 2 == 1 else n - 1 # Kernel scaled to region size (must be odd and >= 3) so small/distant # faces and large/close faces both get proportionally strong blur — # then clamped to fit both of the region's actual dimensions. k = max(3, (min(region_w, region_h) // 3) | 1) k = min(k, _largest_odd_leq(region_w), _largest_odd_leq(region_h)) region = frame[iy1:iy2, ix1:ix2] frame[iy1:iy2, ix1:ix2] = cv2.GaussianBlur(region, (k, k), 0) return frame