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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 302 |
poses: np.ndarray,
|
| 303 |
candidates: np.ndarray,
|
| 304 |
target_positions: np.ndarray,
|
| 305 |
cfg,
|
| 306 |
-
|
| 307 |
-
|
| 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(
|
| 314 |
-
targets_t = torch.as_tensor(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
| 332 |
|
| 333 |
valid_idx = torch.nonzero(valid, as_tuple=False).flatten()
|
| 334 |
valid_candidates = candidates_t.index_select(0, valid_idx)
|
| 335 |
-
|
| 336 |
-
|
| 337 |
if bool(cfg_get(cfg, "training_use_plucker", True)):
|
| 338 |
-
plucker = _plucker_scores_tensor(
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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]
|