lidarverse / utils /visualize /hdmap.py
multimodalart's picture
multimodalart HF Staff
LidarVerse Stage-3 world generation demo
af8bc9f verified
Raw History Blame Contribute Delete
7.07 kB
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
"""Raw-map polylines and translucent actor boxes for PAI quickstart previews.
Map vertices and actors are in the clip reference frame; sensor poses bring
both into the same ego frame as the generated cloud. This is display-only.
"""
import json
import tarfile
import cv2
import numpy as np
from utils.visualize.point_cloud import CAMERA_VIEWS, VIZ_KWARGS
MAP_KINDS = ("lanes", "lanelines", "road_boundaries", "crosswalks", "wait_lines",
"road_markings", "poles", "traffic_signs", "traffic_lights")
MAP_COLORS = ((168, 96, 224), (232, 86, 232), (226, 42, 214), (46, 204, 113),
(255, 214, 44), (255, 214, 44), (150, 92, 224), (131, 175, 155), (252, 157, 155))
BOX_COLORS = ((74, 144, 240), (52, 106, 204), (46, 204, 113), (255, 196, 44), (176, 184, 196))
EDGES = ((0, 1), (1, 2), (2, 3), (3, 0), (4, 5), (5, 6), (6, 7), (7, 4), (0, 4), (1, 5), (2, 6), (3, 7))
def object_class(name):
aliases = {"Bus": "Truck", "Heavy_truck": "Truck", "Train_or_tram_car": "Truck",
"Trolley_bus": "Truck", "Trailer": "Truck", "Vehicle": "Car",
"Automobile": "Car", "Other_vehicle": "Car", "Person": "Pedestrian", "Rider": "Cyclist"}
return {"Car": 0, "Truck": 1, "Pedestrian": 2, "Cyclist": 3}.get(aliases.get(name, name), 4)
def _vertices(node):
if isinstance(node, dict):
for key, value in node.items():
if key == "vertices":
yield np.asarray(value, dtype=np.float32)
else:
yield from _vertices(value)
elif isinstance(node, list):
for item in node:
yield from _vertices(item)
def load_vectors(root, clip):
points, offsets, kinds = [], [0], []
for kind_id, kind in enumerate(MAP_KINDS):
with tarfile.open(root / f"3d_{kind}" / f"{clip}.tar") as archive:
members = [m for m in archive.getmembers() if m.isfile() and m.name.endswith(".json")]
if len(members) != 1:
raise ValueError(f"{clip}/{kind}: expected one vector-map JSON")
with archive.extractfile(members[0]) as handle:
data = json.load(handle)
for vertices in _vertices(data):
if vertices.ndim != 2 or vertices.shape[1] != 3 or not np.isfinite(vertices).all():
raise ValueError(f"{clip}/{kind}: invalid vertices")
if len(vertices) < 2:
continue
if kind in ("traffic_signs", "traffic_lights") and len(vertices) == 8:
vertices = vertices[[0, 1, 2, 3, 0, 4, 5, 6, 7, 4, 5, 1, 2, 6, 7, 3]]
elif kind in ("crosswalks", "road_markings"):
vertices = np.concatenate((vertices, vertices[:1]))
points.append(vertices)
offsets.append(offsets[-1] + len(vertices))
kinds.append(kind_id)
return dict(map_vertices=np.concatenate(points) if points else np.empty((0, 3), np.float32),
map_offsets=np.asarray(offsets, np.int64), map_kinds=np.asarray(kinds, np.int64))
def render_hdmap(sample, frame):
"""Return a white-background BGR frame matching the point-cloud camera."""
for key in ("map_vertices", "map_offsets", "map_kinds", "box_class"):
if key not in sample:
raise ValueError(f"Missing {key}; rerun scripts/data/prepare_pai_bidir.py for raw-map previews")
width, height = VIZ_KWARGS["width"], VIZ_KWARGS["height"]
camera = CAMERA_VIEWS["paper_view_down"]
center = np.array([camera["center"][a] for a in "xyz"]) * VIZ_KWARGS["range"]
eye = np.array([camera["eye"][a] for a in "xyz"]) * VIZ_KWARGS["range"]
eye = center + .6 * (eye - center)
forward = center - eye
forward /= np.linalg.norm(forward)
right = np.cross(forward, [0, 0, 1])
right /= np.linalg.norm(right)
up = np.cross(right, forward)
transform = np.linalg.inv(sample["pose_sensor_start_end"][frame, 0])
tangent = np.tan(np.deg2rad(VIZ_KWARGS.get("fov_deg", 60)) / 2)
def project(points, world=True):
local = points @ transform[:3, :3].T + transform[:3, 3] if world else points
relative = local - eye
depth = relative @ forward
x = (relative @ right / (np.maximum(depth, 1e-3) * tangent * width / height) * .5 + .5) * width
y = (1 - (relative @ up / (np.maximum(depth, 1e-3) * tangent) * .5 + .5 + .08)) * height
return np.clip(np.stack((x, y), -1), -100000, 100000).astype(np.int32), depth, local
image = np.full((height, width, 3), 255, np.uint8)
map_lines = []
box_lines = []
for start, end, kind in zip(sample["map_offsets"][:-1], sample["map_offsets"][1:], sample["map_kinds"]):
pixels, depth, local = project(sample["map_vertices"][start:end])
for i in range(len(pixels) - 1):
if min(depth[i:i+2]) > .1 and np.linalg.norm(local[i:i+2], axis=1).min() < 100:
map_lines.append((pixels[i], pixels[i+1], MAP_COLORS[kind][::-1]))
def box(size, pose, color, world=True):
corners = np.array([[1, 1, 1], [1, -1, 1], [-1, -1, 1], [-1, 1, 1],
[1, 1, -1], [1, -1, -1], [-1, -1, -1], [-1, 1, -1]]) * size / 2
pixels, depth, local = project(corners @ pose[:3, :3].T + pose[:3, 3], world)
if depth.min() <= .1 or np.linalg.norm(local.mean(0)) > 100:
return
overlay = image.copy()
hull = cv2.convexHull(pixels)
cv2.fillConvexPoly(overlay, hull, color, cv2.LINE_AA)
cv2.addWeighted(overlay, .25, image, .75, 0, dst=image)
box_lines.extend((pixels[a], pixels[b], color) for a, b in EDGES)
active = np.flatnonzero(sample["box_valid"][frame])
# Paint distant boxes first. Outlines are drawn after all translucent fills.
origin = sample["pose_sensor_start_end"][frame, 0, :3, 3]
active = sorted(active, key=lambda i: -np.linalg.norm(sample["box_object_to_world"][frame, i, :3, 3] - origin))
for i in active:
box(sample["box_lwh"][frame, i], sample["box_object_to_world"][frame, i],
BOX_COLORS[int(sample["box_class"][frame, i])][::-1])
ego = np.eye(4)
ego[2, 3] = -1.1
box(np.array([4.5, 2., 1.6]), ego, (40, 122, 240), world=False)
for a, b, color in map_lines:
cv2.line(image, tuple(a), tuple(b), color, 1, cv2.LINE_AA)
# Confine the soft halo to box edges; map polylines stay crisp and thin.
halo = np.zeros_like(image, dtype=np.float32)
coverage = np.zeros((height, width), dtype=np.float32)
for a, b, color in box_lines:
cv2.line(halo, tuple(a), tuple(b), color, 3)
cv2.line(coverage, tuple(a), tuple(b), 1., 3)
halo = cv2.GaussianBlur(halo, (5, 5), .8)
coverage = cv2.GaussianBlur(coverage, (5, 5), .8)[..., None]
image = np.clip(image * (1 - .12 * coverage) + .12 * halo, 0, 255).astype(np.uint8)
for a, b, color in box_lines:
cv2.line(image, tuple(a), tuple(b), color, 1, cv2.LINE_AA)
return image