2727import ncore .data
2828import ncore .data .v4
2929import ncore .sensors
30+ from ncore .data import PointCloudsSourceProtocol
3031
3132from 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
0 commit comments