Skip to content

Commit f4bf164

Browse files
committed
feat: migrate ncore point cloud loading to PointCloudsSourceProtocol
1 parent 3d4f902 commit f4bf164

3 files changed

Lines changed: 102 additions & 73 deletions

File tree

examples/datasets/ncore.py

Lines changed: 99 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import ncore.data
2828
import ncore.data.v4
2929
import ncore.sensors
30+
from ncore.data import PointCloudsSourceProtocol
3031

3132
from gsplat.rendering import FThetaCameraDistortionParameters, FThetaPolynomialType
3233

@@ -185,7 +186,7 @@ def __init__(
185186
self.bounds = np.array([0.01, 1.0])
186187
self.extconf = {"spiral_radius_scale": 1.0, "no_factor_suffix": False}
187188

188-
self.points, self.points_rgb = self._load_lidar_points(
189+
self.points, self.points_rgb = self._load_point_clouds(
189190
sequence_loader, max_lidar_points, lidar_step_frame
190191
)
191192

@@ -256,29 +257,30 @@ def _resolve_sensor_ids(
256257

257258
print(f"[NCoreParser] Auto-detected cameras: {camera_ids}")
258259
if not lidar_ids:
259-
lidar_ids = sequence_loader.lidar_ids
260+
lidar_ids = list(sequence_loader.lidar_ids) + list(sequence_loader.point_clouds_ids)
260261

261262
if len(lidar_ids) > 1:
262263
raise ValueError(
263-
"NCoreParser: Multiple lidar sensors in dataset, explicit"
264-
f" specification of a (subset) of lidar sensors required to avoid ambiguity: {lidar_ids}"
264+
"NCoreParser: Multiple point cloud sources in dataset, explicit"
265+
f" specification of a (subset) of sources required to avoid ambiguity: {lidar_ids}"
265266
)
266267

267-
print(f"[NCoreParser] Auto-detected lidars: {lidar_ids}")
268+
print(f"[NCoreParser] Auto-detected point cloud sources: {lidar_ids}")
268269

269270
assert all(
270271
cid in sequence_loader.camera_ids for cid in camera_ids
271272
), f"NCoreParser: some specified camera_ids {camera_ids} not found in dataset cameras {sequence_loader.camera_ids}"
273+
all_point_cloud_ids = set(sequence_loader.lidar_ids) | set(sequence_loader.point_clouds_ids)
272274
assert all(
273-
lid in sequence_loader.lidar_ids for lid in lidar_ids
274-
), f"NCoreParser: some specified lidar_ids {lidar_ids} not found in dataset lidars {sequence_loader.lidar_ids}"
275+
lid in all_point_cloud_ids for lid in lidar_ids
276+
), f"NCoreParser: some specified lidar_ids {lidar_ids} not found in dataset point cloud sources {all_point_cloud_ids}"
275277

276278
self.camera_ids: List[str] = list(camera_ids)
277279
self.lidar_ids: List[str] = list(lidar_ids)
278280
self.num_cameras: int = len(self.camera_ids)
279281

280282
print(f"[NCoreParser] Using cameras: {self.camera_ids}")
281-
print(f"[NCoreParser] Using lidars: {self.lidar_ids}")
283+
print(f"[NCoreParser] Using point cloud sources: {self.lidar_ids}")
282284

283285
def _compute_world_global_transform(
284286
self, sequence_loader: ncore.data.SequenceLoaderProtocol
@@ -579,86 +581,109 @@ def _ncore_world_to_scene_poses(self, T_poses_world: np.ndarray) -> np.ndarray:
579581
T_poses_common = self.T_world_to_scene_world @ T_poses_world.reshape(-1, 4, 4)
580582
return self.world_global_to_scene.transform_poses(T_poses_common)
581583

582-
def _load_lidar_points(
584+
def _load_point_clouds(
583585
self,
584586
sequence_loader: ncore.data.SequenceLoaderProtocol,
585587
max_points: int,
586588
step_frame: int,
587589
) -> Tuple[np.ndarray, np.ndarray]:
588-
"""Load and transform lidar points to scene frame for Gaussian initialisation."""
590+
"""Load and transform point clouds to scene frame for Gaussian initialisation.
591+
592+
Supports any PointCloudsSourceProtocol source (lidar, radar, or native
593+
point clouds) via the unified ``get_point_clouds_source()`` API.
594+
"""
589595
if not self.lidar_ids:
590596
print(
591-
"[NCoreParser] No lidar sensors available; using empty init point cloud"
597+
"[NCoreParser] No point cloud sources available; using empty init point cloud"
592598
)
593599
return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.uint8)
594600

595-
lidar_id = self.lidar_ids[0]
596-
lidar_sensor = sequence_loader.get_lidar_sensor(lidar_id)
597-
lidar_frame_range = self.time_range_us.cover_range(
598-
lidar_sensor.get_frames_timestamps_us()
599-
)
601+
# Pre-compute the world-to-scene transform (identity in "sensor" space = world).
602+
T_world_scene = self._ncore_world_to_scene_poses(
603+
np.eye(4, dtype=np.float32)[np.newaxis]
604+
) # (4, 4)
605+
scale = self.world_global_to_scene.target_scale
600606

601607
all_points: List[np.ndarray] = []
602608
all_colors: List[np.ndarray] = []
603-
for lidar_frame_idx in lidar_frame_range[::step_frame]:
604-
try:
605-
pc = lidar_sensor.get_frame_point_cloud(
606-
frame_index=lidar_frame_idx,
607-
motion_compensation=True,
608-
with_start_points=True,
609-
return_index=0,
610-
)
611-
except Exception as exc:
612-
print(
613-
f"[NCoreParser] Warning: failed to load lidar frame "
614-
f"{lidar_frame_idx}: {exc}"
615-
)
616-
continue
617609

618-
xyz = pc.xyz_m_end
619-
color: Optional[np.ndarray] = None
620-
if lidar_sensor.has_frame_generic_data(
621-
lidar_frame_idx, self.lidar_color_generic_data_name
622-
):
623-
color = lidar_sensor.get_frame_generic_data(
624-
lidar_frame_idx, self.lidar_color_generic_data_name
625-
)
626-
if color.shape != xyz.shape:
627-
raise ValueError(
628-
"Color data length does not match point cloud length "
629-
"(expecting 3-channel RGB color per point)"
610+
for source_id in self.lidar_ids:
611+
source: PointCloudsSourceProtocol = (
612+
sequence_loader.get_point_clouds_source(source_id)
613+
)
614+
ts = source.pc_timestamps_us
615+
616+
for pc_idx in range(source.pcs_count):
617+
# Time filtering
618+
pc_ts = int(ts[pc_idx])
619+
if not (self.time_range_us.start <= pc_ts < self.time_range_us.stop):
620+
continue
621+
622+
# Step frame
623+
if pc_idx % step_frame != 0:
624+
continue
625+
626+
try:
627+
pc = source.get_pc(pc_idx)
628+
except Exception as exc:
629+
print(
630+
f"[NCoreParser] Warning: failed to load point cloud "
631+
f"{pc_idx} from '{source_id}': {exc}"
630632
)
631-
if color.dtype != np.uint8:
632-
raise ValueError("Expected color data in uint8 format")
633-
634-
point_filter = ...
635-
if lidar_sensor.has_frame_generic_data(lidar_frame_idx, "dynamic_flag"):
636-
point_filter = (
637-
lidar_sensor.get_frame_generic_data(lidar_frame_idx, "dynamic_flag")
638-
!= 1
639-
)
640-
xyz = xyz[point_filter]
641-
if color is not None:
642-
color = color[point_filter]
643-
if not len(xyz):
644-
continue
633+
continue
645634

646-
T_sensor_scene = self._ncore_world_to_scene_poses(
647-
lidar_sensor.get_frames_T_sensor_target("world", lidar_frame_idx)
648-
)
649-
xyz_scene = (
650-
(self.world_global_to_scene.target_scale * T_sensor_scene[:3, :3])
651-
@ xyz.T
652-
+ T_sensor_scene[:3, 3:4]
653-
).T
654-
all_points.append(xyz_scene.astype(np.float32))
655-
if color is not None:
656-
all_colors.append(color)
657-
else:
658-
all_colors.append(np.full((len(xyz_scene), 3), 128, dtype=np.uint8))
635+
# Transform to world frame
636+
pc_world = pc.transform(
637+
"world", pc.reference_frame_timestamp_us, sequence_loader.pose_graph
638+
)
639+
xyz_world = pc_world.xyz
640+
641+
# Color: check point cloud attributes first, then source generic data
642+
color: Optional[np.ndarray] = None
643+
if pc.has_attribute(self.lidar_color_generic_data_name):
644+
color = pc.get_attribute(self.lidar_color_generic_data_name)
645+
elif source.has_pc_generic_data(
646+
pc_idx, self.lidar_color_generic_data_name
647+
):
648+
color = source.get_pc_generic_data(
649+
pc_idx, self.lidar_color_generic_data_name
650+
)
651+
if color is not None:
652+
if color.shape != xyz_world.shape:
653+
raise ValueError(
654+
"Color data length does not match point cloud length "
655+
"(expecting 3-channel RGB color per point)"
656+
)
657+
if color.dtype != np.uint8:
658+
raise ValueError("Expected color data in uint8 format")
659+
660+
# Dynamic flag filtering
661+
point_filter = ...
662+
if source.has_pc_generic_data(pc_idx, "dynamic_flag"):
663+
point_filter = (
664+
source.get_pc_generic_data(pc_idx, "dynamic_flag") != 1
665+
)
666+
xyz_world = xyz_world[point_filter]
667+
if color is not None:
668+
color = color[point_filter]
669+
if not len(xyz_world):
670+
continue
671+
672+
# Apply world-to-scene transform
673+
xyz_scene = (
674+
(scale * T_world_scene[:3, :3]) @ xyz_world.T
675+
+ T_world_scene[:3, 3:4]
676+
).T
677+
all_points.append(xyz_scene.astype(np.float32))
678+
if color is not None:
679+
all_colors.append(color)
680+
else:
681+
all_colors.append(
682+
np.full((len(xyz_scene), 3), 128, dtype=np.uint8)
683+
)
659684

660685
if not all_points:
661-
print("[NCoreParser] Warning: no lidar points loaded")
686+
print("[NCoreParser] Warning: no point cloud data loaded")
662687
return np.zeros((0, 3), dtype=np.float32), np.zeros((0, 3), dtype=np.uint8)
663688

664689
points = np.vstack(all_points)
@@ -668,7 +693,10 @@ def _load_lidar_points(
668693
points = points[idx]
669694
points_rgb = points_rgb[idx]
670695

671-
print(f"[NCoreParser] Loaded {len(points)} lidar points from '{lidar_id}'")
696+
source_names = ", ".join(f"'{s}'" for s in self.lidar_ids)
697+
print(
698+
f"[NCoreParser] Loaded {len(points)} point cloud points from {source_names}"
699+
)
672700
return points, points_rgb
673701

674702

examples/requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# assume torch is already installed
2-
nvidia-ncore==18.6.0
2+
nvidia-ncore>=19.0.0
33

44
# pycolmap for data parsing
55
git+https://github.com/rmbrualla/pycolmap@cc7ea4b7301720ac29287dbe450952511b32125e

examples/simple_trainer.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ class Config:
9393
# --- NCore-specific options (only used when data_type="ncore") ---
9494
# Camera sensor IDs to load (auto-detected from sequence if empty)
9595
ncore_camera_ids: List[str] = field(default_factory=list)
96-
# Lidar sensor IDs to load (auto-detected from sequence if empty)
96+
# Point cloud source IDs to load -- accepts lidar, radar, or native point cloud
97+
# source IDs (auto-detected from sequence if empty). Field name kept for backward compat.
9798
ncore_lidar_ids: List[str] = field(default_factory=list)
9899
# Temporal seek offset in seconds
99100
ncore_seek_offset_sec: Optional[float] = None

0 commit comments

Comments
 (0)