Add DeMemWM memory sheet diagnostics
Browse files
algorithms/dememwm/df_video.py
CHANGED
|
@@ -10,7 +10,7 @@ import torch.distributed as dist
|
|
| 10 |
import torch.nn.functional as F
|
| 11 |
import torchvision.transforms.functional as TF
|
| 12 |
from torchvision.transforms import InterpolationMode
|
| 13 |
-
from PIL import Image
|
| 14 |
from einops import rearrange
|
| 15 |
from tqdm import tqdm
|
| 16 |
from omegaconf import DictConfig, open_dict
|
|
@@ -675,6 +675,10 @@ class DeMemWMMinecraft(DiffusionForcingBase):
|
|
| 675 |
self.require_pose_prediction = getattr(cfg, "require_pose_prediction", False)
|
| 676 |
self.log_per_frame_metrics = bool(getattr(cfg, "log_per_frame_metrics", False))
|
| 677 |
self.log_revisit_metrics = bool(getattr(cfg, "log_revisit_metrics", False))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
self.revisit_metrics_min_gap = int(getattr(cfg, "revisit_metrics_min_gap", 8))
|
| 679 |
self.revisit_metrics_gap_bands = tuple(int(x) for x in getattr(cfg, "revisit_metrics_gap_bands", (32, 128)))
|
| 680 |
self.revisit_metrics_self_crop_fraction = float(getattr(cfg, "revisit_metrics_self_crop_fraction", 0.5))
|
|
@@ -891,6 +895,126 @@ class DeMemWMMinecraft(DiffusionForcingBase):
|
|
| 891 |
base = Path(self.local_save_dir) if self.local_save_dir is not None else Path.cwd() / "eval_artifacts"
|
| 892 |
return base / "metrics"
|
| 893 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
def _is_global_rank_zero(self) -> bool:
|
| 895 |
return not (dist.is_available() and dist.is_initialized()) or dist.get_rank() == 0
|
| 896 |
|
|
@@ -1374,6 +1498,8 @@ class DeMemWMMinecraft(DiffusionForcingBase):
|
|
| 1374 |
if dynamic_policy not in {"recent", "event_triggered"}:
|
| 1375 |
raise ValueError(f"memory_selection.dynamic.selection_policy must be recent or event_triggered; got {dynamic_policy!r}")
|
| 1376 |
event_dynamic_caches = [_new_online_event_cache(target_length) for _ in range(batch_size)] if dynamic_policy == "event_triggered" else None
|
|
|
|
|
|
|
| 1377 |
|
| 1378 |
curr_frame = n_context_frames
|
| 1379 |
generated_frames = 0
|
|
@@ -1456,6 +1582,17 @@ class DeMemWMMinecraft(DiffusionForcingBase):
|
|
| 1456 |
else None
|
| 1457 |
),
|
| 1458 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1459 |
selected_masks = {key: np.ones(len(value), dtype=bool) for key, value in selected.items()}
|
| 1460 |
for key, indices in stream_indices.items():
|
| 1461 |
count = indices.shape[1]
|
|
@@ -1562,6 +1699,8 @@ class DeMemWMMinecraft(DiffusionForcingBase):
|
|
| 1562 |
latent_loss = xs_pred.new_tensor(0.0)
|
| 1563 |
|
| 1564 |
self.log(f"{namespace}/latent_mse", latent_loss.detach())
|
|
|
|
|
|
|
| 1565 |
if eval_start < target_length:
|
| 1566 |
xs_pred_decode = self.decode(xs_pred[eval_start:target_length].to(target_poses.device))
|
| 1567 |
xs_decode = self.decode(xs[eval_start:target_length].to(target_poses.device))
|
|
|
|
| 10 |
import torch.nn.functional as F
|
| 11 |
import torchvision.transforms.functional as TF
|
| 12 |
from torchvision.transforms import InterpolationMode
|
| 13 |
+
from PIL import Image, ImageDraw
|
| 14 |
from einops import rearrange
|
| 15 |
from tqdm import tqdm
|
| 16 |
from omegaconf import DictConfig, open_dict
|
|
|
|
| 675 |
self.require_pose_prediction = getattr(cfg, "require_pose_prediction", False)
|
| 676 |
self.log_per_frame_metrics = bool(getattr(cfg, "log_per_frame_metrics", False))
|
| 677 |
self.log_revisit_metrics = bool(getattr(cfg, "log_revisit_metrics", False))
|
| 678 |
+
self.log_memory_selection_sheet = bool(getattr(cfg, "log_memory_selection_sheet", False))
|
| 679 |
+
self.memory_sheet_generated_frames = tuple(
|
| 680 |
+
int(x) for x in getattr(cfg, "memory_sheet_generated_frames", (1, 25, 50, 100, 200, 350, 500))
|
| 681 |
+
)
|
| 682 |
self.revisit_metrics_min_gap = int(getattr(cfg, "revisit_metrics_min_gap", 8))
|
| 683 |
self.revisit_metrics_gap_bands = tuple(int(x) for x in getattr(cfg, "revisit_metrics_gap_bands", (32, 128)))
|
| 684 |
self.revisit_metrics_self_crop_fraction = float(getattr(cfg, "revisit_metrics_self_crop_fraction", 0.5))
|
|
|
|
| 895 |
base = Path(self.local_save_dir) if self.local_save_dir is not None else Path.cwd() / "eval_artifacts"
|
| 896 |
return base / "metrics"
|
| 897 |
|
| 898 |
+
def _memory_sheet_artifact_dir(self) -> Path:
|
| 899 |
+
base = Path(self.local_save_dir) if self.local_save_dir is not None else Path.cwd() / "eval_artifacts"
|
| 900 |
+
return base / "memory_selection_sheets"
|
| 901 |
+
|
| 902 |
+
def _memory_sheet_target_indices(self, eval_start: int, target_length: int) -> set[int]:
|
| 903 |
+
return {
|
| 904 |
+
int(eval_start) + int(frame) - 1
|
| 905 |
+
for frame in self.memory_sheet_generated_frames
|
| 906 |
+
if int(frame) > 0 and int(eval_start) + int(frame) - 1 < int(target_length)
|
| 907 |
+
}
|
| 908 |
+
|
| 909 |
+
def _save_memory_selection_sheets(
|
| 910 |
+
self,
|
| 911 |
+
xs_pred: torch.Tensor,
|
| 912 |
+
xs_gt: torch.Tensor,
|
| 913 |
+
frame_indices: torch.Tensor,
|
| 914 |
+
records: list[list[dict]],
|
| 915 |
+
eval_start: int,
|
| 916 |
+
batch_idx: int,
|
| 917 |
+
) -> None:
|
| 918 |
+
if not self.log_memory_selection_sheet or not self._is_global_rank_zero():
|
| 919 |
+
return
|
| 920 |
+
output_dir = self._memory_sheet_artifact_dir()
|
| 921 |
+
output_dir.mkdir(parents=True, exist_ok=True)
|
| 922 |
+
max_slots = {"anchor": 2, "dynamic": 4, "revisit": 2}
|
| 923 |
+
tile_w, tile_h = 192, 108
|
| 924 |
+
label_h = 34
|
| 925 |
+
gap = 6
|
| 926 |
+
|
| 927 |
+
def decode_indices(source: torch.Tensor, batch_i: int, indices: list[int]) -> dict[int, Image.Image]:
|
| 928 |
+
if not indices:
|
| 929 |
+
return {}
|
| 930 |
+
unique = sorted(set(int(i) for i in indices))
|
| 931 |
+
latents = torch.stack([source[i, batch_i] for i in unique], dim=0).unsqueeze(1)
|
| 932 |
+
with torch.no_grad():
|
| 933 |
+
decoded = self.decode(latents.to(source.device))[:, 0].detach().cpu().float().clamp(0.0, 1.0)
|
| 934 |
+
return {idx: TF.to_pil_image(decoded[row]).resize((tile_w, tile_h), Image.BILINEAR) for row, idx in enumerate(unique)}
|
| 935 |
+
|
| 936 |
+
def make_tile(image: Image.Image | None, label: str, fill=(236, 236, 236)) -> Image.Image:
|
| 937 |
+
tile = Image.new("RGB", (tile_w, tile_h + label_h), fill)
|
| 938 |
+
if image is not None:
|
| 939 |
+
tile.paste(image.convert("RGB"), (0, label_h))
|
| 940 |
+
draw = ImageDraw.Draw(tile)
|
| 941 |
+
draw.rectangle((0, 0, tile_w, label_h), fill=(18, 18, 18))
|
| 942 |
+
for row, line in enumerate(label.split("\n")[:2]):
|
| 943 |
+
draw.text((5, 4 + row * 14), line, fill=(255, 255, 255))
|
| 944 |
+
return tile
|
| 945 |
+
|
| 946 |
+
summary_rows = []
|
| 947 |
+
for batch_i, batch_records in enumerate(records):
|
| 948 |
+
if not batch_records:
|
| 949 |
+
continue
|
| 950 |
+
target_indices = [int(row["target_index"]) for row in batch_records]
|
| 951 |
+
memory_gt = []
|
| 952 |
+
memory_pred = []
|
| 953 |
+
for row in batch_records:
|
| 954 |
+
for stream in _DEMEMWM_STREAM_KEYS:
|
| 955 |
+
for source_index in row["streams"][stream]:
|
| 956 |
+
(memory_gt if source_index < eval_start else memory_pred).append(int(source_index))
|
| 957 |
+
target_gt_images = decode_indices(xs_gt, batch_i, target_indices)
|
| 958 |
+
target_pred_images = decode_indices(xs_pred, batch_i, target_indices)
|
| 959 |
+
memory_images = {}
|
| 960 |
+
memory_images.update(decode_indices(xs_gt, batch_i, memory_gt))
|
| 961 |
+
memory_images.update(decode_indices(xs_pred, batch_i, memory_pred))
|
| 962 |
+
|
| 963 |
+
columns = [("target_gt", -1), ("target_pred", -1)]
|
| 964 |
+
for stream in _DEMEMWM_STREAM_KEYS:
|
| 965 |
+
columns.extend((stream, slot) for slot in range(max_slots[stream]))
|
| 966 |
+
sheet = Image.new(
|
| 967 |
+
"RGB",
|
| 968 |
+
(len(columns) * tile_w + (len(columns) - 1) * gap, len(batch_records) * (tile_h + label_h) + (len(batch_records) - 1) * gap),
|
| 969 |
+
(245, 245, 245),
|
| 970 |
+
)
|
| 971 |
+
for record_row, row in enumerate(batch_records):
|
| 972 |
+
y = record_row * (tile_h + label_h + gap)
|
| 973 |
+
target_index = int(row["target_index"])
|
| 974 |
+
generated_frame = target_index - int(eval_start) + 1
|
| 975 |
+
raw_target = int(row["target_frame"])
|
| 976 |
+
for col, (stream, slot) in enumerate(columns):
|
| 977 |
+
x = col * (tile_w + gap)
|
| 978 |
+
if stream == "target_gt":
|
| 979 |
+
image = target_gt_images.get(target_index)
|
| 980 |
+
label = f"gen{generated_frame:03d} raw{raw_target}\ntarget GT"
|
| 981 |
+
elif stream == "target_pred":
|
| 982 |
+
image = target_pred_images.get(target_index)
|
| 983 |
+
label = f"gen{generated_frame:03d} raw{raw_target}\ntarget pred"
|
| 984 |
+
else:
|
| 985 |
+
stream_indices = row["streams"][stream]
|
| 986 |
+
source_index = int(stream_indices[slot]) if slot < len(stream_indices) else -1
|
| 987 |
+
image = memory_images.get(source_index)
|
| 988 |
+
if source_index >= 0:
|
| 989 |
+
raw_source = int(frame_indices[source_index, batch_i].detach().cpu().item())
|
| 990 |
+
source_type = "ctx" if source_index < eval_start else "pred"
|
| 991 |
+
label = f"{stream}{slot} seq{source_index} raw{raw_source}\n{source_type} for gen{generated_frame:03d}"
|
| 992 |
+
summary_rows.append(
|
| 993 |
+
{
|
| 994 |
+
"batch": batch_idx,
|
| 995 |
+
"sample": batch_i,
|
| 996 |
+
"generated_frame": generated_frame,
|
| 997 |
+
"target_index": target_index,
|
| 998 |
+
"target_raw_frame": raw_target,
|
| 999 |
+
"stream": stream,
|
| 1000 |
+
"slot": slot,
|
| 1001 |
+
"source_index": source_index,
|
| 1002 |
+
"source_raw_frame": raw_source,
|
| 1003 |
+
"source_type": source_type,
|
| 1004 |
+
}
|
| 1005 |
+
)
|
| 1006 |
+
else:
|
| 1007 |
+
label = f"{stream}{slot}\nempty"
|
| 1008 |
+
sheet.paste(make_tile(image, label), (x, y))
|
| 1009 |
+
sheet.save(output_dir / f"batch{batch_idx:06d}_sample{batch_i:02d}_memory_streams.jpg", quality=95)
|
| 1010 |
+
|
| 1011 |
+
if summary_rows:
|
| 1012 |
+
csv_path = output_dir / f"batch{batch_idx:06d}_memory_streams.csv"
|
| 1013 |
+
with csv_path.open("w", newline="", encoding="utf-8") as handle:
|
| 1014 |
+
writer = csv.DictWriter(handle, fieldnames=list(summary_rows[0].keys()))
|
| 1015 |
+
writer.writeheader()
|
| 1016 |
+
writer.writerows(summary_rows)
|
| 1017 |
+
|
| 1018 |
def _is_global_rank_zero(self) -> bool:
|
| 1019 |
return not (dist.is_available() and dist.is_initialized()) or dist.get_rank() == 0
|
| 1020 |
|
|
|
|
| 1498 |
if dynamic_policy not in {"recent", "event_triggered"}:
|
| 1499 |
raise ValueError(f"memory_selection.dynamic.selection_policy must be recent or event_triggered; got {dynamic_policy!r}")
|
| 1500 |
event_dynamic_caches = [_new_online_event_cache(target_length) for _ in range(batch_size)] if dynamic_policy == "event_triggered" else None
|
| 1501 |
+
memory_sheet_targets = self._memory_sheet_target_indices(n_context_frames, target_length) if self.log_memory_selection_sheet else set()
|
| 1502 |
+
memory_sheet_records = [[] for _ in range(batch_size)] if memory_sheet_targets else None
|
| 1503 |
|
| 1504 |
curr_frame = n_context_frames
|
| 1505 |
generated_frames = 0
|
|
|
|
| 1582 |
else None
|
| 1583 |
),
|
| 1584 |
)
|
| 1585 |
+
if memory_sheet_records is not None and int(target_positions[0]) in memory_sheet_targets:
|
| 1586 |
+
memory_sheet_records[batch_i].append(
|
| 1587 |
+
{
|
| 1588 |
+
"target_index": int(target_positions[0]),
|
| 1589 |
+
"target_frame": int(target_frame_indices[int(target_positions[0]), batch_i].detach().cpu().item()),
|
| 1590 |
+
"streams": {
|
| 1591 |
+
key: [int(v) for v in np.asarray(selected.get(key, []), dtype=np.int64) if 0 <= int(v) < curr_frame]
|
| 1592 |
+
for key in _DEMEMWM_STREAM_KEYS
|
| 1593 |
+
},
|
| 1594 |
+
}
|
| 1595 |
+
)
|
| 1596 |
selected_masks = {key: np.ones(len(value), dtype=bool) for key, value in selected.items()}
|
| 1597 |
for key, indices in stream_indices.items():
|
| 1598 |
count = indices.shape[1]
|
|
|
|
| 1699 |
latent_loss = xs_pred.new_tensor(0.0)
|
| 1700 |
|
| 1701 |
self.log(f"{namespace}/latent_mse", latent_loss.detach())
|
| 1702 |
+
if memory_sheet_records is not None:
|
| 1703 |
+
self._save_memory_selection_sheets(xs_pred, xs, target_frame_indices, memory_sheet_records, eval_start, batch_idx)
|
| 1704 |
if eval_start < target_length:
|
| 1705 |
xs_pred_decode = self.decode(xs_pred[eval_start:target_length].to(target_poses.device))
|
| 1706 |
xs_decode = self.decode(xs[eval_start:target_length].to(target_poses.device))
|
datasets/video/memory_selection.py
CHANGED
|
@@ -329,18 +329,18 @@ def _select_anchor(candidates: np.ndarray, count: int, cfg, poses=None) -> np.nd
|
|
| 329 |
poses_t = _as_pose_tensor(poses)
|
| 330 |
candidates_t = torch.as_tensor(candidates.astype(np.int64), device=poses_t.device, dtype=torch.long)
|
| 331 |
candidate_poses = poses_t.index_select(0, candidates_t).float()
|
| 332 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 333 |
if not bool((pairwise > 0).any().item()):
|
| 334 |
return candidates[:count].astype(np.int64)
|
| 335 |
|
| 336 |
available = torch.ones((int(candidates_t.numel()),), device=poses_t.device, dtype=torch.bool)
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
else:
|
| 340 |
-
first, second = divmod(int(pairwise.argmax().item()), int(pairwise.shape[1]))
|
| 341 |
-
selected = [int(first), int(second)]
|
| 342 |
-
for idx in selected:
|
| 343 |
-
available[idx] = False
|
| 344 |
|
| 345 |
dists = pairwise[selected].min(dim=0).values.masked_fill(~available, float("-inf"))
|
| 346 |
for _ in range(count - len(selected)):
|
|
|
|
| 329 |
poses_t = _as_pose_tensor(poses)
|
| 330 |
candidates_t = torch.as_tensor(candidates.astype(np.int64), device=poses_t.device, dtype=torch.long)
|
| 331 |
candidate_poses = poses_t.index_select(0, candidates_t).float()
|
| 332 |
+
spatial = torch.cdist(candidate_poses[:, :3], candidate_poses[:, :3])
|
| 333 |
+
angular = torch.linalg.vector_norm(
|
| 334 |
+
_wrap_degrees(candidate_poses[:, None, 3:] - candidate_poses[None, :, 3:]),
|
| 335 |
+
dim=-1,
|
| 336 |
+
)
|
| 337 |
+
pairwise = torch.sqrt(spatial.square() + angular.square())
|
| 338 |
if not bool((pairwise > 0).any().item()):
|
| 339 |
return candidates[:count].astype(np.int64)
|
| 340 |
|
| 341 |
available = torch.ones((int(candidates_t.numel()),), device=poses_t.device, dtype=torch.bool)
|
| 342 |
+
selected = [0]
|
| 343 |
+
available[0] = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 344 |
|
| 345 |
dists = pairwise[selected].min(dim=0).values.masked_fill(~available, float("-inf"))
|
| 346 |
for _ in range(count - len(selected)):
|