1+ import os
2+ from pathlib import Path
3+ import numpy as np
4+ import argparse
5+ import open3d as o3d
6+ import cv2
7+ import open3d as o3d
8+
9+ def parse_args ():
10+ parser = argparse .ArgumentParser ()
11+ parser .add_argument ("--folder_path" , type = str , required = True )
12+ parser .add_argument ("--output_path" , type = str , required = True )
13+ parser .add_argument ("--camera_types" , type = str , required = True )
14+ return parser .parse_args ()
15+
16+
17+ def save_ply_with_open3d (points , filename ):
18+ pc = o3d .geometry .PointCloud ()
19+ pc .points = o3d .utility .Vector3dVector (points [:, :3 ])
20+ if points .shape [1 ] == 6 :
21+ colors = points [:, 3 :6 ] / 255.0 # normalize RGB to [0, 1]
22+ pc .colors = o3d .utility .Vector3dVector (colors )
23+ o3d .io .write_point_cloud (filename , pc )
24+
25+ def get_camera_extrinsic_matrix (camera_type ):
26+ if camera_type == "front_right" or camera_type == "fr" :
27+ # From your file
28+ rotation = np .array ([
29+ [- 0.7168464770690616 , - 0.10046018208578958 , 0.6899557088168523 ],
30+ [- 0.6970911725372957 , 0.12308618950445319 , - 0.7063382243117325 ],
31+ [- 0.01396515249660048 , - 0.9872981017750231 , - 0.15826380744561577 ]
32+ ])
33+
34+ translation = np .array ([1.8861563355156226 , - 0.7733611068168774 , 1.6793040225335112 ])
35+
36+ # Build 4x4 homogeneous transformation matrix
37+ fr_to_vehicle = np .eye (4 )
38+ fr_to_vehicle [:3 , :3 ] = rotation
39+ fr_to_vehicle [:3 , 3 ] = translation
40+ return fr_to_vehicle
41+ elif camera_type == "rear_right" or camera_type == "rr" :
42+ # From your file
43+ rotation = np .array ([
44+ [- 0.7359657309159472 , 0.15986191414426415 , - 0.6578743127098735 ],
45+ [0.6768157805459531 , 0.14993386619459964 , - 0.7207220233709469 ],
46+ [- 0.016578363047300385 , - 0.9756864271752846 , - 0.21854325362408236 ]
47+ ])
48+
49+ translation = np .array ([0.11419591502518789 , - 0.6896311735924415 , 1.711181163333824 ])
50+
51+ # Build 4x4 homogeneous transformation matrix
52+ rr_to_vehicle = np .eye (4 )
53+ rr_to_vehicle [:3 , :3 ] = rotation
54+ rr_to_vehicle [:3 , 3 ] = translation
55+ return rr_to_vehicle
56+ else :
57+ raise ValueError (f"Camera type { camera_type } not supported" )
58+
59+ def get_camera_intrinsic_matrix (camera_type ):
60+ if camera_type == "front_right" or camera_type == "fr" :
61+ # Provided intrinsics
62+ focal = [1176.2554468073797 , 1175.1456876174707 ] # fx, fy
63+ center = [966.4326452411585 , 608.5803255934914 ] # cx, cy
64+ fr_cam_distort = [- 0.2701363254469883 , 0.16439325523243875 , - 0.001607207824773341 , - 7.412467081891699e-05 ,
65+ - 0.06199397580030171 ]
66+ skew = 0 # assume no skew
67+
68+ fx , fy = focal
69+ cx , cy = center
70+
71+ # Build K matrix
72+ fr_cam_K = np .array ([
73+ [fx , skew , cx ],
74+ [0 , fy , cy ],
75+ [0 , 0 , 1 ]
76+ ])
77+ return fr_cam_K , np .array (fr_cam_distort )
78+ elif camera_type == "rear_right" or camera_type == "rr" :
79+ # Provided intrinsics
80+ focal = [1162.3787554048329 , 1162.855381183851 ] # fx, fy
81+ center = [956.2663906909728 , 569.2039945552984 ] # cx, cy
82+ rr_cam_distort = [- 0.25040910859151444 , 0.1109210921906881 , - 0.00041247665414900384 , 0.0008205455176671751 ,
83+ - 0.026395952816984845 ]
84+ skew = 0 # assume no skew
85+
86+ fx , fy = focal
87+ cx , cy = center
88+
89+ # Build K matrix
90+ rr_cam_K = np .array ([
91+ [fx , skew , cx ],
92+ [0 , fy , cy ],
93+ [0 , 0 , 1 ]
94+ ])
95+ return rr_cam_K , np .array (rr_cam_distort )
96+ else :
97+ raise ValueError (f"Camera type { camera_type } not supported" )
98+
99+ def get_lidar_extrinsic_matrix (lidar_type ):
100+ if lidar_type == "ouster" :
101+ # From your file
102+ rotation = np .array ([
103+ [1 ,0 ,0 ],
104+ [0 ,1 ,0 ],
105+ [0 ,0 ,1 ]
106+ ])
107+
108+ translation = np .array ([1.10 ,0 ,2.03 ])
109+
110+ # Build 4x4 homogeneous transformation matrix
111+ ouster_to_vehicle = np .eye (4 )
112+ ouster_to_vehicle [:3 , :3 ] = rotation
113+ ouster_to_vehicle [:3 , 3 ] = translation
114+ return ouster_to_vehicle
115+ else :
116+ raise ValueError (f"Lidar type { lidar_type } not supported" )
117+
118+ def sample_rgb_colors (image_rgb , u_f , v_f ):
119+ """
120+ Sample RGB colors at subpixel positions (u_f, v_f) from image.
121+ Args:
122+ image_rgb: np.ndarray (H, W, 3), float32 in [0,1]
123+ u_f: (N,) float32
124+ v_f: (N,) float32
125+ Returns:
126+ (N, 3) array of RGB colors
127+ """
128+ assert image_rgb .dtype == np .float32 and image_rgb .max () <= 1.0
129+ colors = []
130+
131+ for x , y in zip (u_f , v_f ):
132+ patch = cv2 .getRectSubPix (image_rgb , patchSize = (1 , 1 ), center = (x , y ))
133+ colors .append (patch [0 , 0 ]) # (1, 1, 3) → get RGB triplet
134+
135+ return np .array (colors )
136+
137+
138+ def main ():
139+ args = parse_args ()
140+ folder_path = args .folder_path
141+ output_path = args .output_path
142+ camera_types = args .camera_types .split (',' )
143+ print ('camera_types' , camera_types )
144+
145+ folder = Path (folder_path )
146+ files = list (folder .glob ("*.b" )) # all files
147+
148+ for file in files :
149+ # print(file.name)
150+ data = np .load (file , allow_pickle = True )
151+ # Save
152+ os .makedirs (output_path , exist_ok = True )
153+ save_ply_with_open3d (data , f"{ output_path } /pure_pointcloud_{ file .name } .ply" )
154+ colors_full = np .zeros ((data .shape [0 ], 3 ), dtype = np .float32 ) # default black
155+ points_h = np .hstack ([data , np .ones ((data .shape [0 ], 1 ))]) # shape: (N, 4)
156+
157+ for camera_type in camera_types :
158+
159+ # Get camera extrinsic matrix
160+ camera_to_vehicle = get_camera_extrinsic_matrix (camera_type )
161+
162+ # Get lidar extrinsic matrix
163+ lidar_to_vehicle = get_lidar_extrinsic_matrix ("ouster" )
164+
165+ # Get camera intrinsic matrix
166+ camera_K , camera_distort = get_camera_intrinsic_matrix (camera_type )
167+
168+ # Project points to camera
169+ # Transform lidar points into camera frame
170+ T_vehicle_to_cam = np .linalg .inv (camera_to_vehicle )
171+ T_lidar_to_cam = T_vehicle_to_cam @ lidar_to_vehicle
172+
173+ # Convert points to homogeneous coordinates
174+
175+ # Transform to camera frame
176+ points_cam = (T_lidar_to_cam @ points_h .T ).T # shape: (N, 4)
177+ points_cam = points_cam [:, :3 ] # drop homogeneous component
178+
179+ # Filter out points behind the camera
180+ mask = points_cam [:, 2 ] > 0
181+ # points_cam = points_cam[mask]
182+
183+ # Load image
184+ point_cloud_number = file .name .split ("_" )[1 ].split ('.' )[0 ]
185+ # image_path = f"{file.parent.name}/{camera_type}_{point_cloud_number}.png"
186+ image_path = file .parent / f"{ camera_type } _{ point_cloud_number } .png"
187+ image_path = str (image_path )
188+ if not Path (image_path ).exists ():
189+ print (f"❌ Image file does not exist: { image_path } " )
190+ continue
191+ image_raw = cv2 .imread (image_path )
192+ h , w = image_raw .shape [:2 ]
193+
194+ cam_distort_matrix = camera_distort
195+ cam_k = camera_K
196+ # Get optimal camera matrix (undistorted intrinsics)
197+ cam_K_new , roi = cv2 .getOptimalNewCameraMatrix (cam_k , cam_distort_matrix , (w , h ), 0 )
198+
199+ # Undistort the image
200+ image_undistorted = cv2 .undistort (image_raw , cam_k , cam_distort_matrix , None , cam_K_new )
201+
202+ # Project to image
203+ proj = (cam_K_new @ points_cam .T ).T # shape: (N, 3)
204+ with np .errstate (divide = 'ignore' , invalid = 'ignore' ):
205+ proj [:, 0 ] = np .divide (proj [:, 0 ], proj [:, 2 ], out = np .zeros_like (proj [:, 0 ]), where = proj [:, 2 ] != 0 )
206+ proj [:, 1 ] = np .divide (proj [:, 1 ], proj [:, 2 ], out = np .zeros_like (proj [:, 1 ]), where = proj [:, 2 ] != 0 )
207+
208+ # proj[:, 0] /= proj[:, 2]
209+ # proj[:, 1] /= proj[:, 2]
210+ pixels = proj [:, :2 ] # shape: (N, 2)
211+
212+ # Step 1: filter front-facing
213+ finite_mask = np .isfinite (proj [:, 0 ]) & np .isfinite (proj [:, 1 ])
214+ # mask_front = mask # Z_cam > 0
215+ mask_front = mask & finite_mask
216+
217+ # Projected pixel coords (u, v)
218+ u = pixels [:, 0 ]
219+ v = pixels [:, 1 ]
220+ if np .any (np .isnan (u )) or np .any (np .isnan (v )):
221+ print (f"❌ NaN values in u or v for { camera_type } " )
222+ continue
223+ if np .any (np .isinf (u )) or np .any (np .isinf (v )):
224+ print (f"❌ Inf values in u or v for { camera_type } " )
225+ continue
226+ # Step 2: in image bounds
227+
228+ # Load image and convert to RGB
229+ image_undistorted = cv2 .cvtColor (image_undistorted , cv2 .COLOR_BGR2RGB )
230+ image_undistorted = image_undistorted .astype (np .float32 ) / 255.0
231+ h , w , _ = image_undistorted .shape
232+ in_bounds = (u >= 0 ) & (u < w - 1 ) & (v >= 0 ) & (v < h - 1 )
233+ # in_bounds = (u >= 0) & (v >= 0)
234+ # Step 3: Combine masks
235+ visible_mask = mask_front & in_bounds
236+ # visible_mask = mask_front
237+
238+ # Step 4: Get visible pixel coords
239+ u_f = u [visible_mask ].astype (np .float32 )
240+ v_f = v [visible_mask ].astype (np .float32 )
241+ if u_f .shape [0 ] == 0 or v_f .shape [0 ] == 0 :
242+ print (f"❌ No visible points for { camera_type } " )
243+ continue
244+ # Step 5: Interpolate RGB
245+ colors_interpolated = sample_rgb_colors (image_undistorted , u_f , v_f )
246+ # Step 6: Assign to full color array
247+ # colors_interpolated = np.concatenate(colors_interpolated, axis=1) # shape: (N, 3)
248+ colors_full [visible_mask ] = colors_interpolated # only visible points get color
249+
250+ # Step 7: Create point cloud with full color info
251+
252+ colored_pcd = o3d .geometry .PointCloud ()
253+ colored_pcd .points = o3d .utility .Vector3dVector (data ) # full point set
254+ colored_pcd .colors = o3d .utility .Vector3dVector (colors_full ) # partial color
255+
256+ o3d .io .write_point_cloud (f"{ output_path } /colored_pointcloud_{ file .name } .ply" , colored_pcd )
257+
258+
259+
260+ if __name__ == "__main__" :
261+ main ()
0 commit comments