BonanDing commited on
Commit
44f6269
·
1 Parent(s): 0691e5a

Add DeMemWM multiview selector backends

Browse files
.exp_artifact/dememwm_dynamic_multiview_memory_selection_plan.md CHANGED
@@ -463,7 +463,7 @@ Make DeMemWM FOV candidates deterministic
463
 
464
  ## Substep 3: Add Deterministic Multiview Selector Backends
465
 
466
- Status: `[ ]`
467
 
468
  Goal:
469
 
 
463
 
464
  ## Substep 3: Add Deterministic Multiview Selector Backends
465
 
466
+ Status: `[x]`
467
 
468
  Goal:
469
 
datasets/video/memory_selection.py CHANGED
@@ -298,25 +298,38 @@ def _best_row(rows: torch.Tensor, gains: torch.Tensor, fov_values: torch.Tensor,
298
  return int(active[0].item())
299
 
300
 
301
- def _select_by_pose_similarity(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
302
  poses: np.ndarray,
303
  candidates: np.ndarray,
304
  target_positions: np.ndarray,
305
  cfg,
306
- count: int,
307
- rng=None,
308
- ) -> np.ndarray:
309
- if count <= 0 or len(candidates) == 0 or len(target_positions) == 0:
310
- return np.empty((0,), dtype=np.int64)
311
 
312
  poses_t = _as_pose_tensor(poses)
313
- candidates_t = torch.as_tensor(candidates.astype(np.int64), device=poses_t.device, dtype=torch.long)
314
- targets_t = torch.as_tensor(target_positions.astype(np.int64), device=poses_t.device, dtype=torch.long)
 
 
 
 
 
315
  candidate_poses = poses_t.index_select(0, candidates_t)
316
  target_poses = poses_t.index_select(0, targets_t)
317
-
318
- # Training preselects with cheap pose similarity; Plucker ranking keeps it
319
- # closer to the inference FOV selector without running the full 10k-point path.
320
  spatial_scale = max(float(cfg_get(cfg, "pose_similarity_radius", _FOV_RADIUS)), 1e-6)
321
  angle_scale = max(float(cfg_get(cfg, "pose_similarity_angle_scale", 180.0)), 1e-6)
322
  spatial_delta = torch.linalg.vector_norm(candidate_poses[:, None, :3] - target_poses[None, :, :3], dim=-1)
@@ -328,30 +341,210 @@ def _select_by_pose_similarity(
328
  threshold = float(cfg_get(cfg, "pose_similarity_threshold", 0.6))
329
  valid = pose_scores >= threshold
330
  if not bool(valid.any()):
331
- return np.empty((0,), dtype=np.int64)
 
 
332
 
333
  valid_idx = torch.nonzero(valid, as_tuple=False).flatten()
334
  valid_candidates = candidates_t.index_select(0, valid_idx)
335
- valid_scores = pose_scores.index_select(0, valid_idx)
336
-
337
  if bool(cfg_get(cfg, "training_use_plucker", True)):
338
- plucker = _plucker_scores_tensor(
339
- poses_t.index_select(0, valid_candidates),
340
- target_poses,
341
- cfg,
342
- )
343
- rank_scores = valid_scores + float(cfg_get(cfg, "training_plucker_weight", 1.0)) * plucker
344
- order = torch.argsort(rank_scores, descending=True)[:count]
345
- selected = valid_candidates.index_select(0, order).cpu().numpy()
346
- else:
347
- valid_np = valid_candidates.cpu().numpy()
348
- size = min(count, len(valid_np))
349
- choice = np.random.choice if rng is None else rng.choice
350
- selected = choice(valid_np, size=size, replace=False)
 
 
 
 
 
 
 
 
351
 
 
352
  return np.sort(np.asarray(selected, dtype=np.int64))
353
 
354
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
355
  def _select_by_point_union(
356
  poses: np.ndarray,
357
  candidates: np.ndarray,
 
298
  return int(active[0].item())
299
 
300
 
301
+ def _deterministic_score_order(scores: torch.Tensor, candidates_t: torch.Tensor) -> torch.Tensor:
302
+ order = np.lexsort((candidates_t.detach().cpu().numpy(), -scores.detach().cpu().numpy()))
303
+ return torch.as_tensor(order, device=candidates_t.device, dtype=torch.long)
304
+
305
+
306
+ def _pose_distance_matrix(left: torch.Tensor, right: torch.Tensor) -> torch.Tensor:
307
+ if left.shape[0] == 0 or right.shape[0] == 0:
308
+ return torch.zeros((left.shape[0], right.shape[0]), device=left.device, dtype=torch.float32)
309
+ right = right.to(device=left.device, dtype=left.dtype)
310
+ translation = torch.linalg.vector_norm(left[:, None, :3] - right[None, :, :3], dim=-1) / _POSE_DISTANCE_SCALE
311
+ angular = torch.linalg.vector_norm(_wrap_degrees(left[:, None, 3:5] - right[None, :, 3:5]), dim=-1) / _ANGLE_DISTANCE_SCALE
312
+ return translation + angular
313
+
314
+
315
+ def _rank_pose_plucker_candidates(
316
  poses: np.ndarray,
317
  candidates: np.ndarray,
318
  target_positions: np.ndarray,
319
  cfg,
320
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
321
+ """Return candidate ids, candidate poses, and deterministic relevance scores."""
 
 
 
322
 
323
  poses_t = _as_pose_tensor(poses)
324
+ candidates_t = torch.as_tensor(np.asarray(candidates, dtype=np.int64), device=poses_t.device, dtype=torch.long)
325
+ targets_t = torch.as_tensor(np.asarray(target_positions, dtype=np.int64), device=poses_t.device, dtype=torch.long)
326
+ if candidates_t.numel() == 0 or targets_t.numel() == 0:
327
+ empty_ids = torch.empty((0,), device=poses_t.device, dtype=torch.long)
328
+ empty_poses = torch.empty((0, 5), device=poses_t.device, dtype=torch.float32)
329
+ return empty_ids, empty_poses, torch.empty((0,), device=poses_t.device)
330
+
331
  candidate_poses = poses_t.index_select(0, candidates_t)
332
  target_poses = poses_t.index_select(0, targets_t)
 
 
 
333
  spatial_scale = max(float(cfg_get(cfg, "pose_similarity_radius", _FOV_RADIUS)), 1e-6)
334
  angle_scale = max(float(cfg_get(cfg, "pose_similarity_angle_scale", 180.0)), 1e-6)
335
  spatial_delta = torch.linalg.vector_norm(candidate_poses[:, None, :3] - target_poses[None, :, :3], dim=-1)
 
341
  threshold = float(cfg_get(cfg, "pose_similarity_threshold", 0.6))
342
  valid = pose_scores >= threshold
343
  if not bool(valid.any()):
344
+ empty_ids = torch.empty((0,), device=poses_t.device, dtype=torch.long)
345
+ empty_poses = torch.empty((0, 5), device=poses_t.device, dtype=torch.float32)
346
+ return empty_ids, empty_poses, torch.empty((0,), device=poses_t.device)
347
 
348
  valid_idx = torch.nonzero(valid, as_tuple=False).flatten()
349
  valid_candidates = candidates_t.index_select(0, valid_idx)
350
+ valid_poses = candidate_poses.index_select(0, valid_idx)
351
+ rank_scores = pose_scores.index_select(0, valid_idx)
352
  if bool(cfg_get(cfg, "training_use_plucker", True)):
353
+ plucker = _plucker_scores_tensor(valid_poses, target_poses, cfg)
354
+ rank_scores = rank_scores + float(cfg_get(cfg, "training_plucker_weight", 1.0)) * plucker
355
+
356
+ order = _deterministic_score_order(rank_scores, valid_candidates)
357
+ return valid_candidates.index_select(0, order), valid_poses.index_select(0, order), rank_scores.index_select(0, order)
358
+
359
+
360
+ def _select_by_pose_similarity(
361
+ poses: np.ndarray,
362
+ candidates: np.ndarray,
363
+ target_positions: np.ndarray,
364
+ cfg,
365
+ count: int,
366
+ rng=None,
367
+ ) -> np.ndarray:
368
+ if count <= 0 or len(candidates) == 0 or len(target_positions) == 0:
369
+ return np.empty((0,), dtype=np.int64)
370
+
371
+ ranked_candidates, _, _ = _rank_pose_plucker_candidates(poses, candidates, target_positions, cfg)
372
+ if ranked_candidates.numel() == 0:
373
+ return np.empty((0,), dtype=np.int64)
374
 
375
+ selected = ranked_candidates[:count].cpu().numpy()
376
  return np.sort(np.asarray(selected, dtype=np.int64))
377
 
378
 
379
+ def _select_pose_fps(
380
+ candidate_ids: torch.Tensor,
381
+ candidate_poses: torch.Tensor,
382
+ count: int,
383
+ *,
384
+ seed_ids: np.ndarray | None = None,
385
+ all_poses: np.ndarray | torch.Tensor | None = None,
386
+ ) -> np.ndarray:
387
+ """Select diverse camera poses from a relevance-filtered candidate set."""
388
+
389
+ candidate_ids = torch.as_tensor(candidate_ids, dtype=torch.long)
390
+ candidate_poses = _as_pose_tensor(candidate_poses).to(device=candidate_ids.device)
391
+ if count <= 0 or candidate_ids.numel() == 0:
392
+ return np.empty((0,), dtype=np.int64)
393
+
394
+ available = torch.ones((candidate_ids.shape[0],), device=candidate_ids.device, dtype=torch.bool)
395
+ selected_rows = []
396
+ seed_poses = torch.empty((0, 5), device=candidate_ids.device, dtype=torch.float32)
397
+ if seed_ids is not None and all_poses is not None:
398
+ all_poses_t = _as_pose_tensor(all_poses).to(device=candidate_ids.device)
399
+ seed_t = torch.as_tensor(np.asarray(seed_ids, dtype=np.int64), device=candidate_ids.device, dtype=torch.long)
400
+ seed_t = seed_t[(seed_t >= 0) & (seed_t < all_poses_t.shape[0])]
401
+ if seed_t.numel() > 0:
402
+ seed_poses = all_poses_t.index_select(0, torch.unique(seed_t, sorted=True))
403
+
404
+ min_dist = None
405
+ if seed_poses.shape[0] > 0:
406
+ min_dist = _pose_distance_matrix(candidate_poses, seed_poses).min(dim=1).values
407
+
408
+ for step in range(min(count, int(candidate_ids.numel()))):
409
+ rows = torch.nonzero(available, as_tuple=False).flatten()
410
+ if rows.numel() == 0:
411
+ break
412
+ if step == 0 and min_dist is None:
413
+ row = int(rows[0].item())
414
+ else:
415
+ vals = min_dist.index_select(0, rows)
416
+ tied = rows.index_select(0, torch.nonzero(vals == vals.max(), as_tuple=False).flatten())
417
+ row = int(tied[torch.argmin(candidate_ids.index_select(0, tied))].item())
418
+ selected_rows.append(row)
419
+ available[row] = False
420
+
421
+ dists = _pose_distance_matrix(candidate_poses, candidate_poses[row : row + 1]).flatten()
422
+ min_dist = dists if min_dist is None else torch.minimum(min_dist, dists)
423
+ min_dist = min_dist.masked_fill(~available, float("-inf"))
424
+
425
+ selected = candidate_ids.index_select(0, torch.as_tensor(selected_rows, device=candidate_ids.device, dtype=torch.long))
426
+ return selected.cpu().numpy().astype(np.int64)
427
+
428
+
429
+ def _select_dynamic_multiview_pose_plucker_fps(
430
+ poses: np.ndarray,
431
+ candidates: np.ndarray,
432
+ target_positions: np.ndarray,
433
+ cfg,
434
+ count: int,
435
+ *,
436
+ seed_ids: np.ndarray | None = None,
437
+ ) -> np.ndarray:
438
+ ranked_ids, ranked_poses, _ = _rank_pose_plucker_candidates(poses, candidates, target_positions, cfg)
439
+ topk = cfg_get(cfg, "pose_preselect_topk", 64)
440
+ if topk is not None:
441
+ topk = int(topk)
442
+ if topk > 0:
443
+ ranked_ids = ranked_ids[:topk]
444
+ ranked_poses = ranked_poses[:topk]
445
+ return _select_pose_fps(ranked_ids, ranked_poses, count, seed_ids=seed_ids, all_poses=poses)
446
+
447
+
448
+ def _select_dynamic_multiview_from_fov_pool(
449
+ pool: dict[str, torch.Tensor],
450
+ count: int,
451
+ *,
452
+ excluded: np.ndarray | None = None,
453
+ reference_frames: np.ndarray | None = None,
454
+ all_poses: np.ndarray | torch.Tensor | None = None,
455
+ ) -> np.ndarray:
456
+ """Select dynamic multiview frames by residual FOV coverage."""
457
+
458
+ if count <= 0:
459
+ return np.empty((0,), dtype=np.int64)
460
+
461
+ candidates_t = pool["candidates_t"]
462
+ candidate_poses = pool["candidate_poses"]
463
+ if candidates_t.numel() == 0:
464
+ return np.empty((0,), dtype=np.int64)
465
+
466
+ valid = torch.ones((candidates_t.shape[0],), device=candidates_t.device, dtype=torch.bool)
467
+ if excluded is not None and len(excluded) > 0:
468
+ excluded_t = torch.as_tensor(np.asarray(excluded, dtype=np.int64), device=candidates_t.device, dtype=torch.long)
469
+ valid &= ~(candidates_t[:, None] == excluded_t[None, :]).any(dim=1)
470
+ if not bool(valid.any()):
471
+ return np.empty((0,), dtype=np.int64)
472
+
473
+ positive = pool.get("positive_fov", torch.zeros_like(valid)).to(device=candidates_t.device, dtype=torch.bool) & valid
474
+ if not bool(positive.any()):
475
+ rows = torch.nonzero(valid, as_tuple=False).flatten()
476
+ return _select_pose_fps(
477
+ candidates_t.index_select(0, rows),
478
+ candidate_poses.index_select(0, rows),
479
+ count,
480
+ seed_ids=reference_frames,
481
+ all_poses=all_poses,
482
+ )
483
+
484
+ rows = torch.nonzero(positive, as_tuple=False).flatten()
485
+ candidates_t = candidates_t.index_select(0, rows)
486
+ candidate_poses = candidate_poses.index_select(0, rows)
487
+ inside = pool["inside"].index_select(0, rows)
488
+ fov_values = pool["fov_values"].index_select(0, rows)
489
+ plucker = pool["plucker"].index_select(0, rows)
490
+ gaps = pool["gaps"].index_select(0, rows)
491
+ temporal_gap = gaps.abs()
492
+
493
+ covered = torch.zeros((inside.shape[1],), device=candidates_t.device, dtype=torch.bool)
494
+ reference_poses = []
495
+ if reference_frames is not None and len(reference_frames) > 0:
496
+ ref_t = torch.as_tensor(np.asarray(reference_frames, dtype=np.int64), device=candidates_t.device, dtype=torch.long)
497
+ ref_rows = torch.nonzero((pool["candidates_t"][:, None] == ref_t[None, :]).any(dim=1), as_tuple=False).flatten()
498
+ if ref_rows.numel() > 0:
499
+ covered |= pool["inside"].index_select(0, ref_rows).any(dim=0)
500
+ reference_poses.append(pool["candidate_poses"].index_select(0, ref_rows))
501
+ if all_poses is not None:
502
+ all_poses_t = _as_pose_tensor(all_poses).to(device=candidates_t.device)
503
+ ref_t = ref_t[(ref_t >= 0) & (ref_t < all_poses_t.shape[0])]
504
+ if ref_t.numel() > 0:
505
+ reference_poses.append(all_poses_t.index_select(0, torch.unique(ref_t, sorted=True)))
506
+
507
+ selected_rows = []
508
+ remaining = torch.ones((candidates_t.shape[0],), device=candidates_t.device, dtype=torch.bool)
509
+ for _ in range(min(count, int(candidates_t.numel()))):
510
+ active = torch.nonzero(remaining, as_tuple=False).flatten()
511
+ if active.numel() == 0:
512
+ break
513
+
514
+ if selected_rows:
515
+ selected_t = torch.as_tensor(selected_rows, device=candidates_t.device, dtype=torch.long)
516
+ refs = reference_poses + [candidate_poses.index_select(0, selected_t)]
517
+ else:
518
+ refs = reference_poses
519
+ if refs:
520
+ reference_pose_t = torch.cat(refs, dim=0)
521
+ pose_dist = _pose_distance_matrix(candidate_poses, reference_pose_t).min(dim=1).values
522
+ else:
523
+ pose_dist = torch.zeros((candidate_poses.shape[0],), device=candidates_t.device, dtype=torch.float32)
524
+ gains = (inside & ~covered[None, :]).float().mean(dim=1)
525
+
526
+ row = active
527
+ for values in (
528
+ gains,
529
+ fov_values,
530
+ plucker,
531
+ pose_dist,
532
+ -temporal_gap.to(dtype=torch.float32),
533
+ -candidates_t.to(dtype=torch.float32),
534
+ ):
535
+ vals = values.index_select(0, row)
536
+ row = row.index_select(0, torch.nonzero(vals == vals.max(), as_tuple=False).flatten())
537
+ if row.numel() == 1:
538
+ break
539
+ selected_row = int(row[0].item())
540
+ selected_rows.append(selected_row)
541
+ covered |= inside[selected_row]
542
+ remaining[selected_row] = False
543
+
544
+ selected = candidates_t.index_select(0, torch.as_tensor(selected_rows, device=candidates_t.device, dtype=torch.long))
545
+ return selected.cpu().numpy().astype(np.int64)
546
+
547
+
548
  def _select_by_point_union(
549
  poses: np.ndarray,
550
  candidates: np.ndarray,
tests/test_dememwm_latent_dataset.py CHANGED
@@ -66,6 +66,38 @@ def _poses(num_frames):
66
  return poses
67
 
68
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  def _dataset_cfg(root, **overrides):
70
  cfg = {
71
  "save_dir": str(root),
@@ -344,6 +376,172 @@ class MemorySelectionTests(unittest.TestCase):
344
  self.assertEqual(indices["revisit"].tolist(), [0, -1])
345
  self.assertEqual(masks["revisit"].tolist(), [True, False])
346
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
347
  def test_training_default_causal_revisit_does_not_select_future(self):
348
  poses = _poses(8)
349
  poses[6] = poses[4]
 
66
  return poses
67
 
68
 
69
+ def _fov_pool(candidate_ids, inside, *, fov_values=None, plucker=None, gaps=None, poses=None):
70
+ candidate_ids = np.asarray(candidate_ids, dtype=np.int64)
71
+ candidates_t = torch.as_tensor(candidate_ids, dtype=torch.long)
72
+ inside_t = torch.as_tensor(inside, dtype=torch.bool)
73
+ if fov_values is None:
74
+ fov_values_t = inside_t.float().mean(dim=1) if inside_t.shape[1] > 0 else torch.zeros((len(candidate_ids),), dtype=torch.float32)
75
+ else:
76
+ fov_values_t = torch.as_tensor(fov_values, dtype=torch.float32)
77
+ if plucker is None:
78
+ plucker_t = torch.zeros((len(candidate_ids),), dtype=torch.float32)
79
+ else:
80
+ plucker_t = torch.as_tensor(plucker, dtype=torch.float32)
81
+ if gaps is None:
82
+ gaps_t = torch.arange(len(candidate_ids), 0, -1, dtype=torch.long)
83
+ else:
84
+ gaps_t = torch.as_tensor(gaps, dtype=torch.long)
85
+ if poses is None:
86
+ candidate_poses = torch.zeros((len(candidate_ids), 5), dtype=torch.float32)
87
+ candidate_poses[:, 0] = candidates_t.to(dtype=torch.float32)
88
+ else:
89
+ candidate_poses = torch.as_tensor(poses[candidate_ids], dtype=torch.float32)
90
+ return {
91
+ "candidates_t": candidates_t,
92
+ "candidate_poses": candidate_poses,
93
+ "inside": inside_t,
94
+ "fov_values": fov_values_t,
95
+ "plucker": plucker_t,
96
+ "gaps": gaps_t,
97
+ "positive_fov": inside_t.any(dim=1),
98
+ }
99
+
100
+
101
  def _dataset_cfg(root, **overrides):
102
  cfg = {
103
  "save_dir": str(root),
 
376
  self.assertEqual(indices["revisit"].tolist(), [0, -1])
377
  self.assertEqual(masks["revisit"].tolist(), [True, False])
378
 
379
+ def test_pose_similarity_refactor_preserves_deterministic_ranked_result(self):
380
+ import datasets.video.memory_selection as memory_selection
381
+
382
+ poses = np.array(
383
+ [
384
+ [0.1, 0, 0, 0, 1],
385
+ [2.0, 0, 0, 0, 4],
386
+ [40.0, 0, 0, 0, 180],
387
+ [0.0, 0, 0, 0, 0],
388
+ ],
389
+ dtype=np.float32,
390
+ )
391
+ cfg = _selection_cfg(pose_similarity_threshold=0.0, training_use_plucker=True, training_plucker_weight=0.0)
392
+
393
+ selected = memory_selection._select_by_pose_similarity(poses, np.asarray([0, 1, 2]), np.asarray([3]), cfg, count=2)
394
+
395
+ self.assertEqual(selected.tolist(), [0, 1])
396
+
397
+ def test_pose_fps_returns_at_most_count_frames(self):
398
+ import datasets.video.memory_selection as memory_selection
399
+
400
+ ids = torch.as_tensor([0, 1], dtype=torch.long)
401
+ poses = torch.as_tensor([[0, 0, 0, 0, 0], [10, 0, 0, 0, 0]], dtype=torch.float32)
402
+
403
+ selected = memory_selection._select_pose_fps(ids, poses, count=5)
404
+
405
+ self.assertEqual(selected.tolist(), [0, 1])
406
+
407
+ def test_pose_fps_is_deterministic(self):
408
+ import datasets.video.memory_selection as memory_selection
409
+
410
+ ids = torch.as_tensor([0, 1, 2], dtype=torch.long)
411
+ poses = torch.as_tensor([[0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [60, 0, 0, 0, 0]], dtype=torch.float32)
412
+
413
+ first = memory_selection._select_pose_fps(ids, poses, count=2)
414
+ second = memory_selection._select_pose_fps(ids, poses, count=2)
415
+
416
+ self.assertEqual(first.tolist(), second.tolist())
417
+
418
+ def test_pose_fps_caps_pose_preselect_topk_before_diversity(self):
419
+ import datasets.video.memory_selection as memory_selection
420
+
421
+ poses = np.zeros((6, 5), dtype=np.float32)
422
+ poses[1, 0] = 1.0
423
+ poses[2, 0] = 30.0
424
+ poses[3, 0] = 60.0
425
+ cfg = _selection_cfg(
426
+ pose_similarity_threshold=0.0,
427
+ training_use_plucker=True,
428
+ training_plucker_weight=0.0,
429
+ pose_preselect_topk=2,
430
+ )
431
+
432
+ selected = memory_selection._select_dynamic_multiview_pose_plucker_fps(
433
+ poses,
434
+ np.asarray([0, 1, 2, 3], dtype=np.int64),
435
+ np.asarray([5], dtype=np.int64),
436
+ cfg,
437
+ count=4,
438
+ )
439
+
440
+ self.assertLessEqual(len(selected), 2)
441
+ self.assertTrue(set(selected.tolist()) <= {0, 1})
442
+
443
+ def test_pose_fps_pose_preselect_topk_zero_is_uncapped(self):
444
+ import datasets.video.memory_selection as memory_selection
445
+
446
+ poses = np.zeros((6, 5), dtype=np.float32)
447
+ poses[1, 0] = 1.0
448
+ poses[2, 0] = 30.0
449
+ poses[3, 0] = 60.0
450
+ cfg = _selection_cfg(
451
+ pose_similarity_threshold=0.0,
452
+ training_use_plucker=True,
453
+ training_plucker_weight=0.0,
454
+ pose_preselect_topk=0,
455
+ )
456
+
457
+ selected = memory_selection._select_dynamic_multiview_pose_plucker_fps(
458
+ poses,
459
+ np.asarray([0, 1, 2, 3], dtype=np.int64),
460
+ np.asarray([5], dtype=np.int64),
461
+ cfg,
462
+ count=4,
463
+ )
464
+
465
+ self.assertEqual(len(selected), 4)
466
+ self.assertEqual(set(selected.tolist()), {0, 1, 2, 3})
467
+
468
+ def test_pose_fps_prefers_diverse_candidate_poses(self):
469
+ import datasets.video.memory_selection as memory_selection
470
+
471
+ ids = torch.as_tensor([0, 1, 2], dtype=torch.long)
472
+ poses = torch.as_tensor([[0, 0, 0, 0, 0], [1, 0, 0, 0, 0], [60, 0, 0, 0, 0]], dtype=torch.float32)
473
+
474
+ selected = memory_selection._select_pose_fps(ids, poses, count=2)
475
+
476
+ self.assertEqual(selected.tolist(), [0, 2])
477
+
478
+ def test_pose_fps_seed_views_make_first_selection_far_when_possible(self):
479
+ import datasets.video.memory_selection as memory_selection
480
+
481
+ all_poses = np.zeros((3, 5), dtype=np.float32)
482
+ all_poses[1, 0] = 1.0
483
+ all_poses[2, 0] = 60.0
484
+ ids = torch.as_tensor([1, 2], dtype=torch.long)
485
+ candidate_poses = torch.as_tensor(all_poses[ids.numpy()], dtype=torch.float32)
486
+
487
+ selected = memory_selection._select_pose_fps(ids, candidate_poses, count=1, seed_ids=np.asarray([0]), all_poses=all_poses)
488
+
489
+ self.assertEqual(selected.tolist(), [2])
490
+
491
+ def test_fov_greedy_dynamic_selection_avoids_excluded_revisit_frames(self):
492
+ import datasets.video.memory_selection as memory_selection
493
+
494
+ pool = _fov_pool([0, 1, 2], [[True, True], [True, False], [False, True]], gaps=[3, 2, 1])
495
+
496
+ selected = memory_selection._select_dynamic_multiview_from_fov_pool(pool, count=1, excluded=np.asarray([0]))
497
+
498
+ self.assertNotIn(0, selected.tolist())
499
+ self.assertEqual(len(selected), 1)
500
+
501
+ def test_fov_greedy_dynamic_selection_is_deterministic(self):
502
+ import datasets.video.memory_selection as memory_selection
503
+
504
+ pool = _fov_pool([0, 1, 2], [[True, False, False], [False, True, False], [False, False, True]], gaps=[3, 2, 1])
505
+
506
+ first = memory_selection._select_dynamic_multiview_from_fov_pool(pool, count=2)
507
+ second = memory_selection._select_dynamic_multiview_from_fov_pool(pool, count=2)
508
+
509
+ self.assertEqual(first.tolist(), second.tolist())
510
+
511
+ def test_fov_greedy_dynamic_selection_uses_absolute_temporal_gap_tie_break(self):
512
+ import datasets.video.memory_selection as memory_selection
513
+
514
+ pool = _fov_pool(
515
+ [4, 10],
516
+ [[True], [True]],
517
+ fov_values=[1.0, 1.0],
518
+ plucker=[0.0, 0.0],
519
+ gaps=[1, -5],
520
+ )
521
+
522
+ selected = memory_selection._select_dynamic_multiview_from_fov_pool(pool, count=1)
523
+
524
+ self.assertEqual(selected.tolist(), [4])
525
+
526
+ def test_fov_greedy_dynamic_selection_prefers_residual_coverage(self):
527
+ import datasets.video.memory_selection as memory_selection
528
+
529
+ pool = _fov_pool(
530
+ [0, 1, 2],
531
+ [[True, False], [True, False], [False, True]],
532
+ plucker=[0.0, 10.0, 0.0],
533
+ gaps=[3, 1, 2],
534
+ )
535
+
536
+ selected = memory_selection._select_dynamic_multiview_from_fov_pool(
537
+ pool,
538
+ count=1,
539
+ excluded=np.asarray([0]),
540
+ reference_frames=np.asarray([0]),
541
+ )
542
+
543
+ self.assertEqual(selected.tolist(), [2])
544
+
545
  def test_training_default_causal_revisit_does_not_select_future(self):
546
  poses = _poses(8)
547
  poses[6] = poses[4]