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:
@@ -59,6 +59,8 @@ context leaks onto the wrong card:
|
|||||||
| `--conf` | 0.3 | detector confidence (model's own recommendation; it is high-precision) |
|
| `--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-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 |
|
| `--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) |
|
| `--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) |
|
| `--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 |
|
| `--imgsz` | 1280 | inference size; model was trained at 1280, `640` is faster at some recall cost |
|
||||||
|
|||||||
+37
-4
@@ -77,6 +77,27 @@ def parse_arguments():
|
|||||||
help="Minimum face box area as a fraction of frame area (kills "
|
help="Minimum face box area as a fraction of frame area (kills "
|
||||||
"background-extra faces in wide shots; 0 disables).",
|
"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(
|
parser.add_argument(
|
||||||
"--conf",
|
"--conf",
|
||||||
type=float,
|
type=float,
|
||||||
@@ -168,10 +189,13 @@ def download_model(path):
|
|||||||
|
|
||||||
|
|
||||||
def face_qualifies(box, frame_shape, min_width, min_height,
|
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
|
"""Original is_full_face(), corrected: the box must actually be a face
|
||||||
(enforced by the detector class), size gates stay, hard
|
(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
|
x1, y1, x2, y2 = box
|
||||||
frame_h, frame_w = frame_shape[:2]
|
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:
|
if w < min_width or h < min_height:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if min_face_fraction > 0:
|
area_frac = (w * h) / float(frame_w * frame_h)
|
||||||
if (w * h) < (frame_w * frame_h * min_face_fraction):
|
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
|
return False
|
||||||
|
|
||||||
if edge_margin > 0:
|
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,
|
if face_qualifies(box, frame.shape, args.min_width,
|
||||||
args.min_height,
|
args.min_height,
|
||||||
args.min_face_fraction,
|
args.min_face_fraction,
|
||||||
|
args.max_face_fraction,
|
||||||
|
args.min_aspect, args.max_aspect,
|
||||||
args.edge_margin):
|
args.edge_margin):
|
||||||
out = os.path.join(
|
out = os.path.join(
|
||||||
output_dir,
|
output_dir,
|
||||||
|
|||||||
Reference in New Issue
Block a user