| """Procedural driving-scene generator (for synthetic data, §2.1). |
| |
| The ground-truth world is built as a set of 3D Gaussians (road surface + lane |
| stripes + curbs + roadside structures + moving vehicles). Rendering it with the |
| same rasterizer yields multi-view-consistent GT images and GT depth, and the HD |
| map (ground height field + lane / boundary polylines) is read off the same |
| scene (optionally with injected noise for the map-robustness study, §4.6). |
| |
| World frame: x = lateral (right), y = forward, z = up. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from dataclasses import dataclass, field |
| from typing import List, Optional |
|
|
| import math |
| import numpy as np |
| import torch |
|
|
| from mapgs.geometry.cameras import look_at_pose |
| from mapgs.hdmap.ground_field import grid_field_from_points, GridGroundField |
| from mapgs.render.gaussians import Gaussians, GROUP_DYNAMIC |
|
|
|
|
| @dataclass |
| class MapNoise: |
| height_std: float = 0.0 |
| lane_offset_std: float = 0.0 |
| drop_prob: float = 0.0 |
|
|
|
|
| @dataclass |
| class ProceduralScene: |
| static: Gaussians |
| dyn_canonical: List[Gaussians] |
| box_centers: torch.Tensor |
| box_rots: torch.Tensor |
| box_size: torch.Tensor |
| canon_idx: torch.Tensor |
| ground: GridGroundField |
| lanes: List[torch.Tensor] |
| boundaries: List[torch.Tensor] |
| K: torch.Tensor |
| cam2world: torch.Tensor |
| F: int |
| V: int |
| fps: int |
|
|
|
|
| def _box_gaussians(g, center, size, color, n, opacity=0.9, jitter_scale=0.06): |
| pts = (g_rand(g, (n, 3)) - 0.5) * torch.tensor(size) |
| means = pts + torch.tensor(center) |
| scales = torch.full((n, 3), jitter_scale) |
| quats = torch.zeros(n, 4); quats[:, 0] = 1 |
| opac = torch.full((n,), opacity) |
| cols = torch.tensor(color)[None].repeat(n, 1) + 0.03 * g_rand(g, (n, 3)) |
| return means.float(), scales.float(), quats.float(), opac.float(), cols.clamp(0, 1).float() |
|
|
|
|
| def g_rand(g, shape): |
| return torch.rand(shape, generator=g) |
|
|
|
|
| def _centerline_x(y, curve_amp, curve_freq): |
| return curve_amp * torch.sin(y * curve_freq) |
|
|
|
|
| def generate_scene( |
| seed: int = 0, |
| F: int = 20, |
| fps: int = 10, |
| H: int = 256, |
| W: int = 448, |
| n_dynamic: int = 2, |
| fov_deg: float = 70.0, |
| curve_amp: float = 2.0, |
| curve_freq: float = 0.02, |
| length: float = 70.0, |
| map_noise: Optional[MapNoise] = None, |
| ) -> ProceduralScene: |
| g = torch.Generator().manual_seed(seed) |
| map_noise = map_noise or MapNoise() |
| V = 3 |
| dt = 1.0 / fps |
|
|
| |
| ys = torch.linspace(-5, length, 200) |
| xs = torch.linspace(-12, 12, 60) |
| GX, GY = torch.meshgrid(xs, ys, indexing="xy") |
| slope = 0.01 |
| base_z = slope * GY + 0.3 * torch.sin(GY * 0.05) |
| gpts = torch.stack([GX.reshape(-1), GY.reshape(-1), base_z.reshape(-1)], -1) |
|
|
| def ground_z(x, y): |
| return slope * y + 0.3 * torch.sin(y * 0.05) |
|
|
| |
| map_pts = gpts.clone().numpy() |
| if map_noise.height_std > 0: |
| map_pts[:, 2] += np.random.RandomState(seed).randn(map_pts.shape[0]) * map_noise.height_std |
| ground = grid_field_from_points(map_pts, spacing=0.7) |
|
|
| |
| yline = torch.linspace(0, length, 120) |
| cx = _centerline_x(yline, curve_amp, curve_freq) |
| lane_off = [-3.5, 0.0, 3.5] |
| bnd_off = [-7.0, 7.0] |
| lanes, boundaries = [], [] |
| rs = np.random.RandomState(seed + 1) |
| for off in lane_off: |
| loff = off + (rs.randn() * map_noise.lane_offset_std) |
| pl = torch.stack([cx + loff, yline, ground_z(cx + loff, yline)], -1) |
| if map_noise.drop_prob > 0: |
| keep = torch.rand(pl.shape[0], generator=g) > map_noise.drop_prob |
| pl = pl[keep] if keep.any() else pl |
| lanes.append(pl.float()) |
| for off in bnd_off: |
| pl = torch.stack([cx + off, yline, ground_z(cx + off, yline)], -1) |
| boundaries.append(pl.float()) |
|
|
| |
| parts = [] |
| |
| ny = 140 |
| yy = torch.linspace(0, length, ny) |
| cxx = _centerline_x(yy, curve_amp, curve_freq) |
| for lane_w in torch.linspace(-6, 6, 25): |
| xw = cxx + lane_w |
| means = torch.stack([xw, yy, ground_z(xw, yy)], -1) |
| scales = torch.full((ny, 3), 0.18); scales[:, 2] = 0.02 |
| quats = torch.zeros(ny, 4); quats[:, 0] = 1 |
| opac = torch.full((ny,), 0.95) |
| gray = 0.32 + 0.04 * g_rand(g, (ny, 1)) |
| cols = gray.repeat(1, 3) |
| parts.append((means, scales, quats, opac, cols)) |
| |
| for off in lane_off: |
| xw = cxx + off |
| dash = (torch.arange(ny) % 6 < 3) |
| means = torch.stack([xw, yy, ground_z(xw, yy) + 0.01], -1)[dash] |
| m = dash.sum() |
| scales = torch.full((m, 3), 0.10); scales[:, 2] = 0.02 |
| quats = torch.zeros(m, 4); quats[:, 0] = 1 |
| opac = torch.full((m,), 0.98) |
| cols = torch.full((m, 3), 0.9) |
| parts.append((means, scales, quats, opac, cols)) |
| |
| n_build = 10 |
| for i in range(n_build): |
| side = 1 if g_rand(g, (1,)).item() > 0.5 else -1 |
| y0 = float(g_rand(g, (1,)).item()) * length |
| x0 = side * (8 + 5 * float(g_rand(g, (1,)).item())) |
| h = 2 + 6 * float(g_rand(g, (1,)).item()) |
| col = (0.3 + 0.5 * g_rand(g, (3,))).tolist() |
| parts.append(_box_gaussians(g, [x0 + _centerline_x(torch.tensor(y0), curve_amp, curve_freq).item(), y0, ground_z(torch.tensor(x0), torch.tensor(y0)).item() + h / 2], |
| [3.0, 3.0, h], col, n=200)) |
|
|
| static = Gaussians( |
| means=torch.cat([p[0] for p in parts]), |
| scales=torch.cat([p[1] for p in parts]), |
| quats=torch.cat([p[2] for p in parts]), |
| opacities=torch.cat([p[3] for p in parts]), |
| colors=torch.cat([p[4] for p in parts]), |
| ) |
|
|
| |
| dyn_canonical: List[Gaussians] = [] |
| box_centers = torch.zeros(n_dynamic, F, 3) |
| box_rots = torch.eye(3).view(1, 1, 3, 3).repeat(n_dynamic, F, 1, 1) |
| box_size = torch.zeros(n_dynamic, 3) |
| canon_idx = torch.zeros(n_dynamic, dtype=torch.long) |
| for i in range(n_dynamic): |
| lane_x = lane_off[i % len(lane_off)] |
| y0 = 10 + 15 * float(g_rand(g, (1,)).item()) |
| speed = 3 + 4 * float(g_rand(g, (1,)).item()) |
| size = [4.2, 1.9, 1.5] |
| box_size[i] = torch.tensor(size) |
| col = (0.2 + 0.6 * g_rand(g, (3,))).tolist() |
| for f in range(F): |
| y = y0 + speed * f * dt |
| x = _centerline_x(torch.tensor(y), curve_amp, curve_freq).item() + lane_x |
| z = ground_z(torch.tensor(x), torch.tensor(y)).item() + size[2] / 2 |
| box_centers[i, f] = torch.tensor([x, y, z]) |
| |
| c0 = box_centers[i, 0].tolist() |
| dyn_canonical.append(Gaussians(*_box_gaussians(g, c0, size, col, n=150))) |
|
|
| |
| fx = 0.5 * W / math.tan(math.radians(fov_deg) / 2) |
| K = torch.tensor([[fx, 0, W / 2], [0, fx, H / 2], [0, 0, 1]]).float()[None].repeat(V, 1, 1) |
| cam_yaws = [math.radians(22), 0.0, math.radians(-22)] |
| cam_lat = [-0.4, 0.0, 0.4] |
| cam2world = torch.zeros(F, V, 4, 4) |
| ego_speed = 6.0 |
| for f in range(F): |
| y_ego = 2 + ego_speed * f * dt |
| x_ego = _centerline_x(torch.tensor(y_ego), curve_amp, curve_freq).item() |
| z_ego = ground_z(torch.tensor(x_ego), torch.tensor(y_ego)).item() + 1.5 |
| fwd = torch.tensor([_centerline_x(torch.tensor(y_ego + 1), curve_amp, curve_freq).item() - x_ego, 1.0, 0.0]) |
| fwd = fwd / fwd.norm() |
| heading = math.atan2(fwd[0].item(), fwd[1].item()) |
| for v in range(V): |
| yaw = heading + cam_yaws[v] |
| eye = torch.tensor([x_ego + cam_lat[v] * math.cos(heading), y_ego, z_ego]) |
| tgt = eye + torch.tensor([math.sin(yaw), math.cos(yaw), -0.05]) * 10.0 |
| cam2world[f, v] = look_at_pose(eye, tgt) |
|
|
| return ProceduralScene( |
| static=static, dyn_canonical=dyn_canonical, box_centers=box_centers, box_rots=box_rots, |
| box_size=box_size, canon_idx=canon_idx, ground=ground, lanes=lanes, boundaries=boundaries, |
| K=K, cam2world=cam2world, F=F, V=V, fps=fps, |
| ) |
|
|