Handle two-frame Ego4D bbox tails and preserve worker errors
This commit is contained in:
+70
-23
@@ -1,4 +1,4 @@
|
||||
"""CPU regressions for a verified Ego4D exclusive-end bbox record."""
|
||||
"""CPU regressions for verified one/two-frame Ego4D terminal bbox overruns."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -23,21 +23,22 @@ from test_local_turn_features import fixture
|
||||
|
||||
|
||||
UID = "31488ff6-05ef-4362-9786-35981b06f307"
|
||||
TWO_FRAME_UID = "ac7d3d16-d74d-4fe3-a1e0-cab0b1adf492"
|
||||
|
||||
|
||||
def terminal_clip():
|
||||
def terminal_clip(*, uid=UID, count=8999, tail_frames=1):
|
||||
return {
|
||||
"clip_uid": UID, "clip_start_frame": 0, "clip_end_frame": 8999,
|
||||
"clip_start_sec": 0, "clip_end_sec": 299.9666666666666,
|
||||
"clip_uid": uid, "clip_start_frame": 0, "clip_end_frame": count,
|
||||
"clip_start_sec": 0, "clip_end_sec": count / 30.,
|
||||
"persons": [{"person_id": "p1", "tracking_paths": [{"track": [
|
||||
{"frame": frame, "clip_frame": None, "x": 0, "y": 0, "width": 2, "height": 2}
|
||||
for frame in (0, 8985, 8998, 8999)
|
||||
for frame in (0, 8985, count - 1, *range(count, count + tail_frames))
|
||||
]}]}],
|
||||
}
|
||||
|
||||
|
||||
def terminal_annotation():
|
||||
clip = terminal_clip()
|
||||
def terminal_annotation(**kwargs):
|
||||
clip = terminal_clip(**kwargs)
|
||||
return {"frames": extract.ego4d_clip_boxes(clip),
|
||||
"bounds": {key: clip[key] for key in (
|
||||
"clip_start_frame", "clip_end_frame", "clip_start_sec", "clip_end_sec",
|
||||
@@ -69,8 +70,33 @@ class Ego4DBoxFrameTests(unittest.TestCase):
|
||||
self.assertIs(result, frames)
|
||||
self.assertEqual(audit["ignored_frame_indices"], [])
|
||||
|
||||
def test_confirmed_two_frame_tail_preserves_last_valid_box_and_counts_people(self):
|
||||
annotation = terminal_annotation(uid=TWO_FRAME_UID, count=8998, tail_frames=2)
|
||||
annotation["frames"][8999].append({"id": "p2", "xyxy": [1, 1, 2, 2]})
|
||||
original = copy.deepcopy(annotation)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
frames, audit = extract.validate_ego4d_box_frames(
|
||||
TWO_FRAME_UID, annotation, SimpleNamespace(num_frames=8998, source_fps=30.))
|
||||
self.assertEqual(frames, {frame: original["frames"][frame] for frame in (0, 8985, 8997)})
|
||||
self.assertEqual(annotation, original)
|
||||
self.assertEqual(audit["ignored_frame_indices"], [8998, 8999])
|
||||
self.assertEqual(audit["ignored_box_count"], 3)
|
||||
self.assertEqual(audit["max_terminal_frame_overrun"], 2)
|
||||
|
||||
def test_sparse_tail_is_bounded_by_position_not_outlier_count(self):
|
||||
annotation = terminal_annotation(count=8998, tail_frames=2)
|
||||
del annotation["frames"][8998]
|
||||
source = SimpleNamespace(num_frames=8998, source_fps=30.)
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
frames, audit = extract.validate_ego4d_box_frames(TWO_FRAME_UID, annotation, source)
|
||||
self.assertEqual(audit["ignored_frame_indices"], [8999])
|
||||
self.assertEqual(sorted(frames), [0, 8985, 8997])
|
||||
annotation["frames"][9999] = annotation["frames"].pop(8999)
|
||||
with self.assertRaisesRegex(ValueError, "out_of_range_count=1"):
|
||||
extract.validate_ego4d_box_frames(TWO_FRAME_UID, annotation, source)
|
||||
|
||||
def test_larger_overrun_negative_and_truncated_source_still_fail(self):
|
||||
for extra in (-1, 9000, 12000):
|
||||
for extra in (-1, 9001, 12000):
|
||||
annotation = terminal_annotation()
|
||||
annotation["frames"][extra] = []
|
||||
with self.subTest(frame=extra), self.assertRaisesRegex(ValueError, "source_frames=8999"):
|
||||
@@ -98,9 +124,15 @@ class Ego4DBoxFrameTests(unittest.TestCase):
|
||||
UID, terminal_annotation(), SimpleNamespace(num_frames=8999, source_fps=fps))
|
||||
|
||||
def test_full_extraction_keeps_1499_rows_saves_audit_and_reuses(self):
|
||||
self._assert_full_extraction_and_reuse(UID, 8999, 1)
|
||||
|
||||
def test_two_frame_tail_extraction_matches_manual_removal_of_unsampled_boxes(self):
|
||||
self._assert_full_extraction_and_reuse(TWO_FRAME_UID, 8998, 2)
|
||||
|
||||
def _assert_full_extraction_and_reuse(self, uid, count, tail_frames):
|
||||
image = Image.new("RGB", (2, 2))
|
||||
source = SimpleNamespace(num_frames=8999, source_fps=30., crop=None, dense_start=0.,
|
||||
path=Path(f"{UID}.mp4"), width=2, height=2)
|
||||
source = SimpleNamespace(num_frames=count, source_fps=30., crop=None, dense_start=0.,
|
||||
path=Path(f"{uid}.mp4"), width=2, height=2)
|
||||
media = SimpleNamespace(
|
||||
clips=[source], metadata=lambda: {},
|
||||
_source_frame_for_time=lambda t: (0, int(t * 30 + 1e-8), int(t * 30 + 1e-8) / 30),
|
||||
@@ -118,24 +150,25 @@ class Ego4DBoxFrameTests(unittest.TestCase):
|
||||
root = Path(temporary)
|
||||
for folder in ("global", "targets", "v1/clips", "v1/annotations"):
|
||||
(root / folder).mkdir(parents=True)
|
||||
reference, _, targets = fixture(n=1499, name=UID, dataset="ego4d")
|
||||
torch.save(reference, root / "global" / f"{UID}.pt")
|
||||
np.save(root / "targets" / f"{UID}.npy", targets)
|
||||
(root / "v1/clips" / f"{UID}.mp4").write_bytes(b"fake-video")
|
||||
for split, clips in (("train", [terminal_clip()]), ("val", [])):
|
||||
reference, _, targets = fixture(n=1499, name=uid, dataset="ego4d")
|
||||
torch.save(reference, root / "global" / f"{uid}.pt")
|
||||
np.save(root / "targets" / f"{uid}.npy", targets)
|
||||
(root / "v1/clips" / f"{uid}.mp4").write_bytes(b"fake-video")
|
||||
clip = terminal_clip(uid=uid, count=count, tail_frames=tail_frames)
|
||||
for split, clips in (("train", [clip]), ("val", [])):
|
||||
(root / f"v1/annotations/av_{split}.json").write_text(json.dumps({"videos": [{"clips": clips}]}))
|
||||
loaded = extract.load_ego4d_boxes([root / "v1/annotations/av_train.json"], [UID])
|
||||
self.assertEqual(loaded[UID], terminal_annotation())
|
||||
loaded = extract.load_ego4d_boxes([root / "v1/annotations/av_train.json"], [uid])
|
||||
self.assertEqual(loaded[uid], terminal_annotation(uid=uid, count=count, tail_frames=tail_frames))
|
||||
argv = ["--dataset", "ego4d", "--data-root", str(root), "--feature-root", str(root / "global"),
|
||||
"--target-root", str(root / "targets"), "--output-root", str(root / "local"),
|
||||
"--samples", UID, "--no-progress"]
|
||||
"--samples", uid, "--no-progress"]
|
||||
with patch.dict(sys.modules, {"transformers": SimpleNamespace(__version__="4.57.6"),
|
||||
"streaming_turn_model_directly": fake_module}), \
|
||||
patch.object(extract, "FrozenQwenLocalEncoder", return_value=encoder) as model, \
|
||||
contextlib.redirect_stdout(io.StringIO()):
|
||||
extract.main(argv)
|
||||
model.assert_called_once()
|
||||
destination = root / "local" / f"{UID}.pt"
|
||||
destination = root / "local" / f"{uid}.pt"
|
||||
result = torch.load(destination, weights_only=False)
|
||||
self.assertEqual(result["visual"].shape, (1499, 6))
|
||||
self.assertEqual(result["audio"].shape, (1499, 5))
|
||||
@@ -143,10 +176,11 @@ class Ego4DBoxFrameTests(unittest.TestCase):
|
||||
self.assertTrue(torch.equal(result["label_indices"], reference["label_indices"]))
|
||||
self.assertAlmostEqual(result["times"][-1].item(), 299.8)
|
||||
self.assertFalse(result["partial"])
|
||||
self.assertEqual(result["box_frame_audit"]["ignored_frame_indices"], [8999])
|
||||
ignored = list(range(count, count + tail_frames))
|
||||
self.assertEqual(result["box_frame_audit"]["ignored_frame_indices"], ignored)
|
||||
self.assertEqual(result["persons"]["frame_times"][-1].item(), 299.5)
|
||||
self.assertEqual(result["persons"]["records"][-1]["source_frame_index"], 8985)
|
||||
load_local_features(destination, reference, sample_id=UID, dataset="ego4d", modalities="av")
|
||||
load_local_features(destination, reference, sample_id=uid, dataset="ego4d", modalities="av")
|
||||
# Previously completed valid sidecars have no audit. Their
|
||||
# vectors/config remain reusable: only unsampled boxes changed.
|
||||
del result["box_frame_audit"]
|
||||
@@ -155,8 +189,21 @@ class Ego4DBoxFrameTests(unittest.TestCase):
|
||||
extract.main(argv + ["--reuse-existing"])
|
||||
model.assert_not_called()
|
||||
extract.main(argv + ["--preview-only", "--preview-count", "1"])
|
||||
audit = json.loads((root / "local" / f"{UID}_preview/box_frame_audit.json").read_text())
|
||||
self.assertEqual(audit["ignored_frame_indices"], [8999])
|
||||
audit = json.loads((root / "local" / f"{uid}_preview/box_frame_audit.json").read_text())
|
||||
self.assertEqual(audit["ignored_frame_indices"], ignored)
|
||||
# The automatic tail handling must produce the same vectors,
|
||||
# masks, timestamps and source-frame records as a clean fixture.
|
||||
track = clip["persons"][0]["tracking_paths"][0]
|
||||
track["track"] = [box for box in track["track"] if box["frame"] < count]
|
||||
(root / "v1/annotations/av_train.json").write_text(json.dumps({"videos": [{"clips": [clip]}]}))
|
||||
extract.main(argv + ["--output-root", str(root / "clean")])
|
||||
clean = torch.load(root / "clean" / f"{uid}.pt", weights_only=False)
|
||||
for key in ("visual", "audio", "visual_valid", "audio_valid", "times", "label_indices"):
|
||||
self.assertTrue(torch.equal(result[key], clean[key]), key)
|
||||
for key in ("features", "frame_times", "frame_offsets"):
|
||||
self.assertTrue(torch.equal(result["persons"][key], clean["persons"][key]), key)
|
||||
self.assertEqual(result["persons"]["records"], clean["persons"]["records"])
|
||||
self.assertEqual(clean["box_frame_audit"]["ignored_frame_indices"], [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
Reference in New Issue
Block a user