move_detect/tests/test_faceblur.py
유창욱 7a86545367 feat(delivery-audit): privacy — face-blur sizing + model integrity (P3-1, P3-3)
- P3-1 [H] _head_boxes sizes the anonymization region from the person bbox (scales with
  distance) + bbox-top fallback when the nose keypoint is low-confidence; a fixed ~40px
  patch had left close faces identifiable. test_faceblur.py.
- P3-3 [H] fetch_movenet.py rewritten to stage-validate (reject HTML/LFS-pointer, optional
  pinned sha256+size, fail loud) per the SHA256 spec requirement. Discovery: the old PINTO
  raw URL is DEAD (GitHub 404 → it was silently saving an HTML page as the .onnx). Non-blocking
  for the Hailo delivery path; the CPU MoveNet fallback needs a valid MOVENET_ONNX_URL pinned.

python + phpunit suites green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019Xmn8KZWqiQUguEMRtn6rW
2026-07-17 09:35:02 +09:00

25 lines
1.2 KiB
Python

"""Face-anonymization sizing (PIPA). _head_boxes must scale the blur region with the person's
size — a fixed ~40x40px patch leaves a close worker's face fully identifiable, defeating the
anonymization the spec requires. No hardware: synthetic Person/BBox."""
from edge.app import _head_boxes
from edge.models import Person, BBox, Keypoint
def _person(bbox, nose_conf=0.9):
nose = Keypoint(bbox.cx, bbox.y1 + 0.1 * bbox.height, nose_conf)
return Person(bbox, [nose] + [Keypoint(bbox.cx, bbox.cy, nose_conf) for _ in range(16)])
def test_head_box_scales_with_person_size():
small = _head_boxes([_person(BBox(0, 0, 40, 120))])[0]
big = _head_boxes([_person(BBox(0, 0, 400, 1200))])[0] # 10x bigger person
assert big.width > small.width * 5, "head box must scale with the person, not be fixed"
assert big.width > 40, "a close face must be covered by more than the old ~40px box"
def test_head_box_falls_back_to_bbox_top_when_nose_low_confidence():
p = _person(BBox(0, 0, 100, 300), nose_conf=0.1) # nose keypoint unreliable
box = _head_boxes([p])[0]
assert box.width > 0 and box.height > 0
# anchored to the head region (top of the bbox), not wherever the low-conf nose landed
assert box.y1 < p.bbox.cy