fix: face traces split at scene cuts — even same person, different cut

This commit is contained in:
Accusys
2026-05-14 00:21:17 +08:00
parent 9007e46b9f
commit 74f00d3baa
2 changed files with 40 additions and 5 deletions

View File

@@ -62,8 +62,17 @@ def run_face_tracker(face_json_path: str, traced_json_path: str) -> str:
} }
print(f"[TRACE] Processing {len(face_data.get('frames', {}))} frames") print(f"[TRACE] Processing {len(face_data.get('frames', {}))} frames")
face_data = track_faces(face_data, use_embedding=True) # Load cut boundaries from cut.json (same directory as face.json)
cut_boundaries = None
cuts_path = face_json_path.replace("_traced.json", ".cut.json").replace(".face.json", ".cut.json")
if os.path.exists(cuts_path):
with open(cuts_path) as f:
cuts = json.load(f)
cut_boundaries = {s["start_frame"] for s in cuts.get("scenes", []) if s["start_frame"] > 0}
print(f"[TRACE] Loaded {len(cut_boundaries)} cut boundaries")
face_data = track_faces(face_data, use_embedding=True, cut_boundaries=cut_boundaries)
metadata = face_data.get("metadata", {}) metadata = face_data.get("metadata", {})
metadata["tracking_method"] = "iou_embedding" metadata["tracking_method"] = "iou_embedding"
metadata["tracked_at"] = datetime.now().isoformat() metadata["tracked_at"] = datetime.now().isoformat()

View File

@@ -28,7 +28,7 @@ Output:
import json import json
import argparse import argparse
import numpy as np import numpy as np
from typing import Dict, List from typing import Dict, List, Set
from collections import defaultdict from collections import defaultdict
@@ -112,6 +112,9 @@ def match_faces(
distance_threshold: float = 100.0, distance_threshold: float = 100.0,
use_embedding: bool = True, use_embedding: bool = True,
frame_gap: int = 1, frame_gap: int = 1,
cut_boundaries: Set[int] = None,
prev_frame: int = None,
curr_frame: int = None,
) -> Dict[int, int]: ) -> Dict[int, int]:
""" """
Match current frame faces to previous frame faces Match current frame faces to previous frame faces
@@ -124,6 +127,9 @@ def match_faces(
distance_threshold: Maximum bbox center distance for matching distance_threshold: Maximum bbox center distance for matching
use_embedding: Whether to use embedding similarity use_embedding: Whether to use embedding similarity
frame_gap: Number of frames between current and previous (1=adjacent) frame_gap: Number of frames between current and previous (1=adjacent)
cut_boundaries: Set of frame numbers where scene cuts occur
prev_frame: Previous frame number (for cut detection)
curr_frame: Current frame number (for cut detection)
Returns: Returns:
Dict mapping current_face_index -> previous_face_index (or -1 if new) Dict mapping current_face_index -> previous_face_index (or -1 if new)
@@ -131,6 +137,12 @@ def match_faces(
if not previous_faces: if not previous_faces:
return {i: -1 for i in range(len(current_faces))} return {i: -1 for i in range(len(current_faces))}
# If a scene cut exists between prev and current frame, force all new traces
if cut_boundaries and prev_frame is not None and curr_frame is not None:
for cf in cut_boundaries:
if prev_frame < cf <= curr_frame:
return {i: -1 for i in range(len(current_faces))}
matches = {} matches = {}
used_prev = set() used_prev = set()
@@ -193,8 +205,6 @@ def match_faces(
if prev_at_edge and not curr_at_edge and similarity < 0.8: if prev_at_edge and not curr_at_edge and similarity < 0.8:
continue continue
if iou > iou_threshold and similarity > similarity_threshold:
if iou > iou_threshold and similarity > similarity_threshold: if iou > iou_threshold and similarity > similarity_threshold:
score = iou + similarity score = iou + similarity
elif iou > 0.5 and similarity > 0.65: elif iou > 0.5 and similarity > 0.65:
@@ -226,6 +236,7 @@ def track_faces(
similarity_threshold: float = 0.7, similarity_threshold: float = 0.7,
distance_threshold: float = 100.0, distance_threshold: float = 100.0,
use_embedding: bool = True, use_embedding: bool = True,
cut_boundaries: Set[int] = None,
) -> Dict: ) -> Dict:
""" """
Track faces across all frames Track faces across all frames
@@ -254,6 +265,7 @@ def track_faces(
prev_faces = [] prev_faces = []
prev_trace_ids = [] prev_trace_ids = []
prev_frame_num = None prev_frame_num = None
prev_face_frame = None # last frame number that had actual faces
print(f"\nTracking faces across {len(sorted_frames)} frames...") print(f"\nTracking faces across {len(sorted_frames)} frames...")
print(f"Parameters: iou={iou_threshold}, similarity={similarity_threshold}, distance={distance_threshold}") print(f"Parameters: iou={iou_threshold}, similarity={similarity_threshold}, distance={distance_threshold}")
@@ -279,6 +291,9 @@ def track_faces(
distance_threshold, distance_threshold,
use_embedding, use_embedding,
frame_gap, frame_gap,
cut_boundaries,
prev_face_frame,
frame_num,
) )
trace_ids = [] trace_ids = []
@@ -307,6 +322,7 @@ def track_faces(
prev_faces = faces prev_faces = faces
prev_trace_ids = trace_ids prev_trace_ids = trace_ids
prev_face_frame = frame_num
if frame_num % 100 == 0: if frame_num % 100 == 0:
print(f" Frame {frame_num}: {len(faces)} faces, {len(set(trace_ids))} active traces") print(f" Frame {frame_num}: {len(faces)} faces, {len(set(trace_ids))} active traces")
@@ -455,9 +471,18 @@ def main():
parser.add_argument("--similarity-threshold", type=float, default=0.7, help="Embedding similarity threshold") parser.add_argument("--similarity-threshold", type=float, default=0.7, help="Embedding similarity threshold")
parser.add_argument("--distance-threshold", type=float, default=100.0, help="Distance threshold") parser.add_argument("--distance-threshold", type=float, default=100.0, help="Distance threshold")
parser.add_argument("--no-embedding", action="store_true", help="Disable embedding matching") parser.add_argument("--no-embedding", action="store_true", help="Disable embedding matching")
parser.add_argument("--cuts-json", help="Path to cut.json for scene-cut-aware tracking")
parser.add_argument("--analyze-only", action="store_true", help="Only analyze, don't output") parser.add_argument("--analyze-only", action="store_true", help="Only analyze, don't output")
args = parser.parse_args() args = parser.parse_args()
# Load cut boundaries if provided
cut_boundaries = None
if args.cuts_json:
with open(args.cuts_json) as f:
cuts = json.load(f)
cut_boundaries = {s["start_frame"] for s in cuts.get("scenes", []) if s["start_frame"] > 0}
print(f" Cut boundaries loaded: {len(cut_boundaries)} cuts")
print("=" * 60) print("=" * 60)
print("Face Tracker") print("Face Tracker")
print("=" * 60) print("=" * 60)
@@ -474,6 +499,7 @@ def main():
similarity_threshold=args.similarity_threshold, similarity_threshold=args.similarity_threshold,
distance_threshold=args.distance_threshold, distance_threshold=args.distance_threshold,
use_embedding=not args.no_embedding, use_embedding=not args.no_embedding,
cut_boundaries=cut_boundaries,
) )
analyze_traces(face_data) analyze_traces(face_data)