Skip to content

Commit a84c3e9

Browse files
committed
Merge branch 'EstCal_A' of https://github.com/krishauser/GEMstack into EstCal_A
2 parents c914a41 + 88ec6ba commit a84c3e9

3 files changed

Lines changed: 179 additions & 0 deletions

File tree

.DS_Store

8 KB
Binary file not shown.

GEMstack/.DS_Store

6 KB
Binary file not shown.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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("\nBest 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

Comments
 (0)