MissawB's picture
Release Point-DiT EMA weights, code and model card
4fe19fe verified
Raw History Blame Contribute Delete
4.7 kB
import os
import torch
import numpy as np
from torch.utils.data import Dataset
from typing import Optional
from data.smal_quadrupeds import SMALQuadrupedGenerator, FAMILIES, PARENT_INDICES, GENERATOR_VERSION
from data.ot_coupling import precompute_ot_noise
class CreatureDataset(Dataset):
"""PyTorch Dataset for 3D creatures conditioned on kinematic skeletons."""
def __init__(
self,
num_samples_per_family: int = 50,
num_points: int = 2048,
augment: bool = True,
cache_dir: Optional[str] = "data/cache",
seed: int = 42,
ot_noise_per_sample: int = 0,
independent_noise_prob: float = 0.0,
ot_workers: Optional[int] = None
):
super().__init__()
self.num_points = num_points
self.augment = augment
self.cache_dir = cache_dir
self.independent_noise_prob = independent_noise_prob
self.samples = []
self.ot_noise = None
generator = SMALQuadrupedGenerator(seed=seed)
# Cache file is keyed on generation parameters so a config change never reuses stale samples
cache_file = os.path.join(cache_dir, f"dataset_v{GENERATOR_VERSION}_n{num_samples_per_family}_p{num_points}_s{seed}.pt") if cache_dir else None
if cache_file and os.path.exists(cache_file):
cached = torch.load(cache_file, weights_only=False)
self.samples = cached["samples"]
else:
for family in FAMILIES:
for _ in range(num_samples_per_family):
instance = generator.generate_instance(family=family, num_points=num_points)
self.samples.append(instance)
if cache_file:
os.makedirs(cache_dir, exist_ok=True)
torch.save({"samples": self.samples}, cache_file)
if ot_noise_per_sample > 0:
self.ot_noise = self._load_or_compute_ot_noise(cache_file, ot_noise_per_sample, seed, ot_workers)
def _load_or_compute_ot_noise(self, cache_file: Optional[str], num_noise: int, seed: int, workers: Optional[int]):
noise_file = cache_file.replace(".pt", f"_ot{num_noise}.pt") if cache_file else None
if noise_file and os.path.exists(noise_file):
return torch.load(noise_file, weights_only=True)["noise"]
noise = precompute_ot_noise([s["points"] for s in self.samples], num_noise, seed=seed, num_workers=workers)
noise = [torch.from_numpy(n) for n in noise]
if noise_file:
torch.save({"noise": noise}, noise_file)
return noise
def __len__(self) -> int:
return len(self.samples)
def _apply_augmentation(self, points: np.ndarray, joints: np.ndarray, noise: Optional[np.ndarray] = None):
# Coherent rotation around vertical Y axis
theta = np.random.uniform(-np.pi, np.pi)
cos_t, sin_t = np.cos(theta), np.sin(theta)
R = np.array([
[cos_t, 0.0, sin_t],
[0.0, 1.0, 0.0 ],
[-sin_t, 0.0, cos_t]
], dtype=np.float32)
points = points @ R
joints = joints @ R
# N(0, I) is rotation invariant: rotating the matched noise keeps it Gaussian and keeps the OT pairing
if noise is not None:
noise = noise @ R
# Scaling jitter
scale = np.random.uniform(0.92, 1.08)
points *= scale
joints *= scale
return points, joints, noise
def __getitem__(self, idx: int):
raw = self.samples[idx]
pts = raw["points"].copy()
jts = raw["joints"].copy()
noise = None
if self.ot_noise is not None:
if np.random.rand() < self.independent_noise_prob:
# Hybrid coupling: some independent noise keeps the field easy to learn near t = 0
noise = np.random.randn(*pts.shape).astype(np.float32)
else:
stack = self.ot_noise[idx]
noise = stack[np.random.randint(len(stack))].numpy().copy()
if self.augment:
pts, jts, noise = self._apply_augmentation(pts, jts, noise)
item = {
"points": torch.from_numpy(pts).float(),
"joints": torch.from_numpy(jts).float(),
"joint_parents": torch.from_numpy(raw["joint_parents"]).long(),
"family_id": torch.tensor(raw["family_id"]).long()
}
if noise is not None:
item["noise"] = torch.from_numpy(noise).float()
return item