Handle verified Ego4D terminal bounding-box records

This commit is contained in:
Xin Wang
2026-08-28 01:11:51 +08:00
parent 5fd285e369
commit 668996bfa8
4 changed files with 246 additions and 6 deletions

View File

@@ -175,7 +175,14 @@ def load_ego4d_boxes(paths: list[Path], names: list[str]) -> dict[str, Any]:
if uid in selected:
if uid in result:
raise ValueError(f"Duplicate Ego4D annotation UID: {uid}")
result[uid] = ego4d_clip_boxes(clip)
# Retain declared bounds so a terminal record can be
# distinguished from a truncated video or frame mismatch.
result[uid] = {
"frames": ego4d_clip_boxes(clip),
"bounds": {key: clip.get(key) for key in (
"clip_start_frame", "clip_end_frame", "clip_start_sec", "clip_end_sec",
)},
}
del payload
missing = set(names) - result.keys()
if missing:
@@ -183,6 +190,45 @@ def load_ego4d_boxes(paths: list[Path], names: list[str]) -> dict[str, Any]:
return result
def validate_ego4d_box_frames(name: str, annotation: dict[str, Any], source):
"""Ignore only a confirmed exclusive-end record; never shift/clamp boxes."""
frames, bounds = annotation["frames"], annotation["bounds"]
count = source.num_frames
outside = sorted(frame for frame in frames if not 0 <= frame < count)
audit = {
"policy": "clip-local-zero-based; ignore-confirmed-exclusive-end-only",
"source_frames": count, "source_fps": source.source_fps,
"declared_bounds": dict(bounds),
"annotation_frame_range": [min(frames), max(frames)] if frames else None,
"ignored_frame_indices": [], "ignored_box_count": 0,
}
if not outside:
return frames, audit
start_sec, end_sec = bounds.get("clip_start_sec"), bounds.get("clip_end_sec")
duration_matches = (
isinstance(start_sec, (int, float)) and isinstance(end_sec, (int, float))
and np.isfinite([start_sec, end_sec, source.source_fps]).all()
and source.source_fps > 0 and abs(start_sec) <= 1e-6
and abs((end_sec - start_sec) - count / source.source_fps) <= 1e-4
)
if (outside != [count] or bounds.get("clip_start_frame") != 0
or bounds.get("clip_end_frame") != count or not duration_matches):
raise ValueError(
f"Ego4D annotation frame exceeds source clip: {name}; "
f"source_frames={count}, source_fps={source.source_fps}, "
f"annotation_range={audit['annotation_frame_range']}, "
f"out_of_range_count={len(outside)}, first_10={outside[:10]}, "
f"declared_bounds={bounds}. Only frame == source_frames == clip_end_frame "
"with zero-origin, matching duration is an allowed terminal record; "
"check annotation/video alignment instead of clamping or skipping the clip."
)
audit.update(ignored_frame_indices=[count], ignored_box_count=len(frames[count]))
print(f"{name}: ignoring exclusive-end bbox record at frame {count} "
f"({len(frames[count])} boxes); decoded frames are 0..{count - 1}. "
"No frame shift or feature/target row removal.", flush=True)
return {frame: boxes for frame, boxes in frames.items() if frame != count}, audit
def build_media_and_boxes(args: argparse.Namespace, name: str, end: float, ego_boxes: dict[str, Any]):
# Lazy imports allow --help and pure tests without Transformers/decoders.
from streaming_turn_model_directly import CausalSourceAV, SourceClipSpec, _easycom_directory_specs
@@ -209,9 +255,9 @@ def build_media_and_boxes(args: argparse.Namespace, name: str, end: float, ego_b
raise ValueError(f"Box/video frame inventory mismatch: {path}")
annotations.append(mapping)
else:
annotations = [ego_boxes[name]]
if annotations[0] and max(annotations[0]) >= media.clips[0].num_frames:
raise ValueError(f"Ego4D annotation frame exceeds source clip: {name}")
mapping, audit = validate_ego4d_box_frames(name, ego_boxes[name], media.clips[0])
annotations = [mapping]
ego_boxes[name]["frame_audit"] = audit
return media, annotations
@@ -622,6 +668,9 @@ def main(argv=None) -> None:
"annotations use --box-coordinate-space encoded; otherwise boxes shift right.", flush=True)
if args.preview_only:
preview_boxes(media, annotations, args, name, len(times) / 5)
if args.dataset == "ego4d":
audit_path = args.output_root / f"{name}_preview" / "box_frame_audit.json"
audit_path.write_text(json.dumps(ego_boxes[name]["frame_audit"], indent=2) + "\n")
ego_boxes.pop(name, None)
del media, annotations, reference
continue
@@ -643,6 +692,10 @@ def main(argv=None) -> None:
"reference_features": str(path.resolve()), "direct_media": media.metadata(),
"config": config,
})
if args.dataset == "ego4d":
# Per-clip diagnostics, not encoder config: existing valid sidecars
# retain the same reuse contract and need not be re-extracted.
output["box_frame_audit"] = ego_boxes[name]["frame_audit"]
destination = args.output_root / f"{name}.pt"
# Atomic replacement: a failed extraction never looks like a ready file.
with tempfile.NamedTemporaryFile(dir=args.output_root, prefix=f".{name}.", suffix=".tmp", delete=False) as handle: