Add geometric upper-bound gates: max face-area fraction, aspect window

Tested against real failure samples from the v2 run: full-frame feather
storm (area .64) and wide effect blob (area .70) now rejected; normal
and close-up faces pass. Note: stylized OP/ED emblems are geometrically
face-shaped and NOT catchable this way — documented, CCIP gate is the
answer for those.
This commit is contained in:
2026-10-18 11:04:53 -04:00
parent 940c59d6c9
commit 89b0f708d7
2 changed files with 39 additions and 4 deletions
+2
View File
@@ -59,6 +59,8 @@ context leaks onto the wrong card:
| `--conf` | 0.3 | detector confidence (model's own recommendation; it is high-precision) |
| `--min-width` / `--min-height` | 100 | minimum face box size in px |
| `--min-face-fraction` | 0.005 | face box area as fraction of frame — kills distant background extras; `0` disables |
| `--max-face-fraction` | 0.30 | face box area upper cap — kills detector meltdowns on full-frame ED/OP effects (feather storms etc.); `0` disables |
| `--min-aspect` / `--max-aspect` | 0.45 / 1.6 | face box W/H ratio window — effect blobs rarely keep face proportions |
| `--edge-margin` | 0 | require faces this far inside frame edges; `0` accepts edge-touching faces (original hard-rejected them) |
| `--scene-threshold` | 30.0 | PySceneDetect ContentDetector threshold (same as original) |
| `--imgsz` | 1280 | inference size; model was trained at 1280, `640` is faster at some recall cost |
+37 -4
View File
@@ -77,6 +77,27 @@ def parse_arguments():
help="Minimum face box area as a fraction of frame area (kills "
"background-extra faces in wide shots; 0 disables).",
)
parser.add_argument(
"--max-face-fraction",
type=float,
default=0.30,
help="Maximum face box area as a fraction of frame area (kills "
"detector meltdowns on full-frame effects like ED feather "
"storms; 0 disables).",
)
parser.add_argument(
"--min-aspect",
type=float,
default=0.45,
help="Minimum face box width/height ratio.",
)
parser.add_argument(
"--max-aspect",
type=float,
default=1.6,
help="Maximum face box width/height ratio (a face wider than 1.6x "
"itself is almost always an effect blob).",
)
parser.add_argument(
"--conf",
type=float,
@@ -168,10 +189,13 @@ def download_model(path):
def face_qualifies(box, frame_shape, min_width, min_height,
min_face_fraction, edge_margin):
min_face_fraction, max_face_fraction,
min_aspect, max_aspect, edge_margin):
"""Original is_full_face(), corrected: the box must actually be a face
(enforced by the detector class), size gates stay, hard
'must-not-touch-edge' rule becomes an optional margin."""
'must-not-touch-edge' rule becomes an optional margin.
Upper-bound gates (max area, aspect) kill detector meltdowns on
full-frame stylized effects (ED feather storms etc.)."""
x1, y1, x2, y2 = box
frame_h, frame_w = frame_shape[:2]
@@ -179,8 +203,15 @@ def face_qualifies(box, frame_shape, min_width, min_height,
if w < min_width or h < min_height:
return False
if min_face_fraction > 0:
if (w * h) < (frame_w * frame_h * min_face_fraction):
area_frac = (w * h) / float(frame_w * frame_h)
if min_face_fraction > 0 and area_frac < min_face_fraction:
return False
if max_face_fraction > 0 and area_frac > max_face_fraction:
return False
if h > 0:
aspect = w / float(h)
if aspect < min_aspect or aspect > max_aspect:
return False
if edge_margin > 0:
@@ -255,6 +286,8 @@ def process_video(video_path, output_dir, args, model, device, draw,
if face_qualifies(box, frame.shape, args.min_width,
args.min_height,
args.min_face_fraction,
args.max_face_fraction,
args.min_aspect, args.max_aspect,
args.edge_margin):
out = os.path.join(
output_dir,