| """Per-epoch augmentation + subsampling for cached scene samples. |
| |
| These transforms apply to the dict returned by `HoHoCachedDataset` *after* it |
| has been loaded from a semantic cache shard. They mutate `scene_xyz`, the per-point |
| parallel arrays, and `verts_gt[:, :3]` in place-equivalent ways (returning |
| new tensors) so that: |
| |
| - Random 3D rotation: rotate scene + GT vertices by a random 3D rotation matrix. |
| - Random horizontal flip: optional X→−X mirror, applied to scene + GT. |
| - Random XYZ jitter: small Gaussian noise on `scene_xyz` only. |
| - Optional random subsample to `n_pts`: only active when the cache has more |
| points than the configured model input. At eval time we instead take a |
| deterministic prefix slice. |
| |
| All operations are torch-native to keep work on whichever device the |
| DataLoader returns; in practice they run on CPU before the batch ships to |
| the GPU. |
| """ |
| from __future__ import annotations |
|
|
| from typing import Optional |
|
|
| import torch |
|
|
|
|
| |
| POINT_KEYS = ( |
| "scene_xyz", |
| "scene_type_ids", |
| "scene_gestalt_ids", |
| "scene_gestalt_id2", |
| "scene_gestalt_w1", |
| "scene_ade_ids", |
| "scene_geom_conf", |
| "scene_sem_conf", |
| "scene_rgb", |
| ) |
|
|
|
|
| def _yaw_rotation_matrix(theta: torch.Tensor) -> torch.Tensor: |
| """3x3 rotation around Y (Y-up). theta: scalar tensor in radians.""" |
| c = torch.cos(theta) |
| s = torch.sin(theta) |
| R = torch.eye(3, dtype=torch.float32, device=theta.device) |
| R[0, 0] = c |
| R[0, 2] = s |
| R[2, 0] = -s |
| R[2, 2] = c |
| return R |
|
|
|
|
| def random_yaw_rotation(sample: dict, generator: Optional[torch.Generator] = None) -> dict: |
| """Rotate scene_xyz and verts_gt[:, :3] by the same random Y-axis angle.""" |
| theta = torch.rand((), generator=generator) * (2.0 * torch.pi) |
| R = _yaw_rotation_matrix(theta) |
|
|
| sample["scene_xyz"] = sample["scene_xyz"] @ R.T |
| if "verts_gt" in sample: |
| v = sample["verts_gt"] |
| |
| v_xyz = v[:, :3] @ R.T |
| sample["verts_gt"] = torch.cat([v_xyz, v[:, 3:4]], dim=-1) |
| return sample |
|
|
|
|
| def random_horizontal_flip(sample: dict, p: float = 0.5, |
| generator: Optional[torch.Generator] = None) -> dict: |
| """With probability `p`, mirror the scene along X (X → −X).""" |
| if torch.rand((), generator=generator).item() >= p: |
| return sample |
| sample["scene_xyz"] = sample["scene_xyz"] * torch.tensor([-1.0, 1.0, 1.0]) |
| if "verts_gt" in sample: |
| v = sample["verts_gt"] |
| v_xyz = v[:, :3] * torch.tensor([-1.0, 1.0, 1.0]) |
| sample["verts_gt"] = torch.cat([v_xyz, v[:, 3:4]], dim=-1) |
| return sample |
|
|
|
|
| def random_xyz_jitter(sample: dict, sigma: float = 0.01, |
| generator: Optional[torch.Generator] = None) -> dict: |
| """Add small Gaussian noise to scene_xyz only (verts_gt stays clean).""" |
| if sigma <= 0.0: |
| return sample |
| noise = torch.randn(sample["scene_xyz"].shape, generator=generator) * sigma |
| sample["scene_xyz"] = sample["scene_xyz"] + noise |
| return sample |
|
|
|
|
| def random_point_subsample(sample: dict, n_pts: int, |
| generator: Optional[torch.Generator] = None) -> dict: |
| """Randomly keep `n_pts` of the cached points (training augmentation).""" |
| n_cache = sample["scene_xyz"].shape[0] |
| if n_cache == n_pts: |
| return sample |
| if n_cache < n_pts: |
| |
| extra = torch.randint(0, n_cache, (n_pts - n_cache,), generator=generator) |
| idx = torch.cat([torch.arange(n_cache), extra]) |
| else: |
| idx = torch.randperm(n_cache, generator=generator)[:n_pts] |
| for k in POINT_KEYS: |
| if k in sample: |
| sample[k] = sample[k][idx] |
| return sample |
|
|
|
|
| def deterministic_point_slice(sample: dict, n_pts: int) -> dict: |
| """Take the first `n_pts` cached points (eval/inference use).""" |
| n_cache = sample["scene_xyz"].shape[0] |
| if n_cache == n_pts: |
| return sample |
| if n_cache < n_pts: |
| |
| last = n_cache - 1 |
| pad = torch.full((n_pts - n_cache,), last, dtype=torch.long) |
| idx = torch.cat([torch.arange(n_cache), pad]) |
| else: |
| idx = torch.arange(n_pts) |
| for k in POINT_KEYS: |
| if k in sample: |
| sample[k] = sample[k][idx] |
| return sample |
|
|
|
|
| def apply_train_transforms( |
| sample: dict, |
| n_pts: int, |
| yaw: bool = True, |
| flip: bool = True, |
| jitter_sigma: float = 0.01, |
| generator: Optional[torch.Generator] = None, |
| ) -> dict: |
| """Full training-time augmentation pipeline. Order: subsample → flip → yaw → jitter.""" |
| sample = random_point_subsample(sample, n_pts, generator=generator) |
| if flip: |
| sample = random_horizontal_flip(sample, generator=generator) |
| if yaw: |
| sample = random_yaw_rotation(sample, generator=generator) |
| if jitter_sigma > 0.0: |
| sample = random_xyz_jitter(sample, sigma=jitter_sigma, generator=generator) |
| return sample |
|
|
|
|
| def apply_eval_transforms(sample: dict, n_pts: int) -> dict: |
| """Eval/inference-time pipeline: deterministic slice, no rotation, no jitter.""" |
| return deterministic_point_slice(sample, n_pts) |
|
|