1+ #!/usr/bin/env python3
2+
3+ import os
4+ import glob
5+ import numpy as np
6+ import cv2
7+ import open3d as o3d
8+
9+ # --- Adjust these to match your intrinsics and data details ---
10+ INTRINSICS = np .array ([
11+ [684.83331299 , 0. , 573.37109375 ],
12+ [ 0. , 684.60968018 , 363.70092773 ],
13+ [ 0. , 0. , 1. ]
14+ ], dtype = np .float32 )
15+
16+ # Depth scale: how to go from raw .tif values to actual "Z" in meters.
17+ # In your capture code, you used: dimage = (dimage/4000*0xffff)
18+ # Possibly each pixel is stored with range 0..65535. Adjust as needed.
19+ DEPTH_SCALE = 4000.0 # Example factor if your depth was in mm or a certain scale.
20+
21+ def depth_to_points (depth_img : np .ndarray , intrinsics : np .ndarray ):
22+ """
23+ Convert a single-channel depth image into an Nx3 array of 3D points
24+ in the camera coordinate system.
25+ - depth_img: 2D array of type uint16 or float with depth values
26+ - intrinsics: 3x3 camera matrix
27+ """
28+ fx = intrinsics [0 ,0 ]
29+ fy = intrinsics [1 ,1 ]
30+ cx = intrinsics [0 ,2 ]
31+ cy = intrinsics [1 ,2 ]
32+
33+ # Indices of each pixel
34+ h , w = depth_img .shape
35+ i_range = np .arange (h )
36+ j_range = np .arange (w )
37+ jj , ii = np .meshgrid (j_range , i_range ) # shape (h,w)
38+
39+ # Flatten
40+ ii = ii .flatten ().astype (np .float32 )
41+ jj = jj .flatten ().astype (np .float32 )
42+ depth = depth_img .flatten ().astype (np .float32 )
43+
44+ # Filter out zero / invalid depths
45+ valid_mask = (depth > 0 )
46+ ii = ii [valid_mask ]
47+ jj = jj [valid_mask ]
48+ depth = depth [valid_mask ]
49+
50+ # Reproject to 3D (camera frame)
51+ # X = (x - cx) * Z / fx, Y = (y - cy) * Z / fy, Z = depth
52+ # Note: Z must be in meters or consistent units
53+ z = depth / DEPTH_SCALE # Convert from your .tif scale to real meters
54+ x = (jj - cx ) * z / fx
55+ y = (ii - cy ) * z / fy
56+ points3d = np .stack ((x , y , z ), axis = - 1 ) # shape (N,3)
57+
58+ return points3d
59+
60+ def load_lidar_points (npz_file : str ):
61+ """
62+ Load the Nx3 LiDAR points from a .npz file created by 'np.savez'.
63+ """
64+ data = np .load (npz_file )
65+ # The capture code used: np.savez(lidar_fn, pc)
66+ # So 'data' might have the default key 'arr_0' or 'pc' if named
67+ # Inspect data.files to see. Let's assume 'arr_0' or single key:
68+ arr_key = data .files [0 ]
69+ points = data [arr_key ]
70+ # shape check, must be Nx3 or Nx4, ...
71+ return points
72+
73+ def make_open3d_pcd (points : np .ndarray ):
74+ """
75+ Convert Nx3 numpy array into an Open3D point cloud object.
76+ """
77+ pcd = o3d .geometry .PointCloud ()
78+ pcd .points = o3d .utility .Vector3dVector (points )
79+ return pcd
80+
81+ def perform_icp (camera_pcd : o3d .geometry .PointCloud ,
82+ lidar_pcd : o3d .geometry .PointCloud ):
83+ """
84+ Perform local ICP alignment of camera_pcd (source) to lidar_pcd (target).
85+ Returns a transformation 4x4 that maps camera -> lidar (or vice versa).
86+ """
87+ # 1) Estimate normals if you want point-to-plane
88+ lidar_pcd .estimate_normals (search_param = o3d .geometry .KDTreeSearchParamHybrid (
89+ radius = 1.0 , max_nn = 30 ))
90+ camera_pcd .estimate_normals (search_param = o3d .geometry .KDTreeSearchParamHybrid (
91+ radius = 1.0 , max_nn = 30 ))
92+
93+ # 2) Initial guess: identity or something better if you have one
94+ init_guess = np .eye (4 )
95+
96+ # 3) Choose a threshold for inlier distance
97+ threshold = 0.5 # adjust based on your environment scale
98+
99+ # 4) Run ICP (point-to-plane or point-to-point)
100+ print ("[ICP] Running ICP alignment ..." )
101+ result = o3d .pipelines .registration .registration_icp (
102+ camera_pcd , # source
103+ lidar_pcd , # target
104+ threshold ,
105+ init_guess ,
106+ estimation_method = o3d .pipelines .registration .TransformationEstimationPointToPlane ()
107+ )
108+
109+ print ("[ICP] Done. Fitness = %.4f, RMSE = %.4f" % (result .fitness , result .inlier_rmse ))
110+ print ("[ICP] Transformation:\n " , result .transformation )
111+ return result .transformation
112+
113+ def main (folder = 'data' ):
114+ color_files = sorted (glob .glob (os .path .join (folder , "color*.png" )))
115+ icp_results = [] # to store (index, transform, fitness, rmse)
116+
117+ for color_path in color_files :
118+ # Extract index from filename, e.g. color10.png -> 10
119+ basename = os .path .basename (color_path )
120+ idx_str = basename .replace ("color" ,"" ).replace (".png" ,"" )
121+
122+ if int (idx_str ) in range (77 ):
123+ depth_path = os .path .join (folder , f"depth{ idx_str } .tif" )
124+ lidar_path = os .path .join (folder , f"lidar{ idx_str } .npz" )
125+
126+ if not (os .path .exists (depth_path ) and os .path .exists (lidar_path )):
127+ continue
128+
129+ # Load depth / convert to 3D
130+ depth_img = cv2 .imread (depth_path , cv2 .IMREAD_UNCHANGED )
131+ if depth_img is None :
132+ continue
133+ camera_points = depth_to_points (depth_img , INTRINSICS )
134+ camera_pcd = make_open3d_pcd (camera_points )
135+
136+ # Load LiDAR
137+ lidar_points = load_lidar_points (lidar_path )
138+ lidar_pcd = make_open3d_pcd (lidar_points )
139+
140+ lidar_pcd .estimate_normals (
141+ search_param = o3d .geometry .KDTreeSearchParamHybrid (
142+ radius = 1.0 , # Adjust based on your scene scale
143+ max_nn = 30
144+ )
145+ )
146+
147+ # Also estimate normals on the Camera (source) cloud (recommended)
148+ camera_pcd .estimate_normals (
149+ search_param = o3d .geometry .KDTreeSearchParamHybrid (
150+ radius = 1.0 ,
151+ max_nn = 30
152+ )
153+ )
154+
155+ # Now you can run Point-to-Plane ICP
156+ init_guess = np .eye (4 )
157+ threshold = 0.5 # or some appropriate distance
158+ result_icp = o3d .pipelines .registration .registration_icp (
159+ camera_pcd ,
160+ lidar_pcd ,
161+ threshold ,
162+ init_guess ,
163+ o3d .pipelines .registration .TransformationEstimationPointToPlane ()
164+ )
165+
166+ print ("ICP result:" , result_icp .transformation )
167+ print ("Fitness:" , result_icp .fitness , " RMSE:" , result_icp .inlier_rmse )
168+
169+ # After processing all frames, we can analyze or average
170+ if len (icp_results ) > 0 :
171+ # Example: pick the best frame by highest fitness
172+ best_frame = max (icp_results , key = lambda x : x [2 ]) # x[2] is fitness
173+ print ("\n Best frame by fitness was index=" , best_frame [0 ],
174+ " with fitness=" , best_frame [2 ], " rmse=" , best_frame [3 ])
175+ else :
176+ print ("No frames processed properly." )
177+
178+ if __name__ == "__main__" :
179+ main ()
0 commit comments