2026_s23dr / data /transforms.py
jskvrna's picture
Update to stage 2
b1666b5
Raw
History Blame Contribute Delete
5.38 kB
"""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
# Per-point arrays that must be co-permuted when subsampling/dropping points.
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[:, :3] only — keep the validity flag at column 3 untouched.
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:
# Pad with random repeats — should be rare since cache is built oversized.
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:
# Cache shorter than expected — pad by repeating the last point.
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)