Handle verified Ego4D terminal bounding-box records
This commit is contained in:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user