| """Render map geometry into camera views. |
| |
| * :func:`rasterize_map_depth` casts each pixel ray against the ground height |
| field (a fixed-point intersection) to get per-view ground depth ``D^map`` and |
| the ground/lane mask ``Omega^g``. ``D^map`` is a supervision *target* |
| (constant w.r.t. the Gaussians), so it is computed without gradients and |
| fully vectorized over pixels — no triangle z-buffer needed. |
| * :func:`project_polylines` projects lane / boundary vertices into each view |
| (used as the map side of the lane-chamfer term, §2.6 item 3, and for lane |
| mIoU, §4.4). |
| * :func:`render_lane_mask` splats projected lane points into a soft raster. |
| """ |
|
|
| from __future__ import annotations |
|
|
| from typing import List |
|
|
| import torch |
|
|
| from mapgs.geometry.cameras import camera_centers |
| from mapgs.hdmap.ground_field import GroundField |
|
|
|
|
| @torch.no_grad() |
| def rasterize_map_depth( |
| ground: GroundField, |
| K: torch.Tensor, |
| cam2world: torch.Tensor, |
| H: int, |
| W: int, |
| iters: int = 16, |
| near: float = 0.5, |
| far: float = 200.0, |
| min_descent: float = 1e-3, |
| tol: float = 0.25, |
| ): |
| """Ray/height-field intersection -> ``depth [V,H,W]``, ``mask [V,H,W]`` (bool).""" |
| device = K.device |
| V = K.shape[0] |
| dtype = K.dtype |
|
|
| vv, uu = torch.meshgrid( |
| torch.arange(H, device=device, dtype=dtype) + 0.5, |
| torch.arange(W, device=device, dtype=dtype) + 0.5, |
| indexing="ij", |
| ) |
| ones = torch.ones_like(uu) |
| pix = torch.stack([uu, vv, ones], dim=-1) |
| Kinv = torch.inverse(K) |
| r_cam = torch.einsum("vij,hwj->vhwi", Kinv, pix) |
| R_c2w = cam2world[:, :3, :3] |
| m = torch.einsum("vij,vhwj->vhwi", R_c2w, r_cam) |
| o = camera_centers(cam2world)[:, None, None, :] |
|
|
| mz = m[..., 2] |
| descending = mz < -min_descent |
| |
| h0, _ = ground.height_at(o[..., :2].expand(V, H, W, 2)) |
| Z = (h0 - o[..., 2]) / mz.clamp(max=-min_descent) |
| Z = Z.clamp(near, far) |
| for _ in range(iters): |
| xy = o[..., :2] + Z.unsqueeze(-1) * m[..., :2] |
| h, _ = ground.height_at(xy) |
| Z = ((h - o[..., 2]) / mz.clamp(max=-min_descent)).clamp(near, far) |
|
|
| xy = o[..., :2] + Z.unsqueeze(-1) * m[..., :2] |
| h, valid_xy = ground.height_at(xy) |
| residual = (o[..., 2] + Z * mz - h).abs() |
| mask = descending & valid_xy & (residual < tol) & (Z > near) & (Z < far) |
| depth = torch.where(mask, Z, torch.zeros_like(Z)) |
| return depth, mask |
|
|
|
|
| @torch.no_grad() |
| def project_polylines( |
| polylines, |
| K: torch.Tensor, |
| cam2world: torch.Tensor, |
| H: int, |
| W: int, |
| ) -> List[torch.Tensor]: |
| """Project map polyline vertices into each view. Returns list (len V) of ``[Mi, 2]`` uv.""" |
| from mapgs.geometry.cameras import project_points |
| from mapgs.geometry.transforms import se3_inverse |
|
|
| if isinstance(polylines, (list, tuple)): |
| pts = torch.cat([p for p in polylines if p.numel() > 0], 0) if len(polylines) else torch.zeros(0, 3) |
| else: |
| pts = polylines |
| device = K.device |
| pts = pts.to(device) |
| V = K.shape[0] |
| out: List[torch.Tensor] = [] |
| if pts.numel() == 0: |
| return [torch.zeros(0, 2, device=device) for _ in range(V)] |
| w2c = se3_inverse(cam2world) |
| for v in range(V): |
| uv, z = project_points(pts[None], K[v : v + 1], w2c[v : v + 1]) |
| uv = uv[0] |
| z = z[0] |
| inb = (z > 0.1) & (uv[:, 0] >= 0) & (uv[:, 0] < W) & (uv[:, 1] >= 0) & (uv[:, 1] < H) |
| out.append(uv[inb]) |
| return out |
|
|
|
|
| def render_lane_mask(uv: torch.Tensor, H: int, W: int, radius: float = 2.0) -> torch.Tensor: |
| """Splat projected lane points ``[M, 2]`` into a soft mask ``[H, W]`` in [0,1].""" |
| device = uv.device |
| if uv.numel() == 0: |
| return torch.zeros(H, W, device=device) |
| vv, uu = torch.meshgrid( |
| torch.arange(H, device=device, dtype=torch.float32), |
| torch.arange(W, device=device, dtype=torch.float32), |
| indexing="ij", |
| ) |
| |
| grid = torch.stack([uu, vv], dim=-1).reshape(-1, 2) |
| min_d2 = torch.full((grid.shape[0],), 1e9, device=device) |
| for i in range(0, uv.shape[0], 4096): |
| chunk = uv[i : i + 4096] |
| d2 = (grid[:, None, :] - chunk[None, :, :]).pow(2).sum(-1).min(dim=1).values |
| min_d2 = torch.minimum(min_d2, d2) |
| mask = torch.exp(-min_d2 / (2 * radius ** 2)) |
| return mask.reshape(H, W) |
|
|