Skip to content

Commit 3b22a85

Browse files
committed
added point cloud matching tools
1 parent 84a145a commit 3b22a85

2 files changed

Lines changed: 169 additions & 0 deletions

File tree

src/py_utils/icp.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import numpy as np
2+
import pptk.kdtree
3+
4+
from . import utils
5+
from . import utils_align
6+
7+
8+
def _build_kd_tree(points):
9+
return pptk.kdtree._build(points)
10+
11+
12+
def nearest_neighbors(src_points, kdtree_or_dst_points, dmax=np.inf):
13+
14+
kdtree = kdtree_or_dst_points
15+
if isinstance(kdtree, np.ndarray):
16+
kdtree = _build_kd_tree(kdtree)
17+
18+
indices = pptk.kdtree._query(kdtree, src_points, k=1, dmax=dmax)
19+
20+
src_indices = []
21+
dst_indices = []
22+
for src_idx, dst_idx in enumerate(indices):
23+
24+
if len(dst_idx) == 0:
25+
continue
26+
27+
src_indices.append(src_idx)
28+
dst_indices.append(dst_idx[0])
29+
30+
return np.array(src_indices), np.array(dst_indices)
31+
32+
33+
def iterative_closest_point(
34+
src_points,
35+
dst_points,
36+
initial_pose=np.eye(4),
37+
# optimization parameters
38+
max_iterations=20,
39+
max_distance=np.inf,
40+
min_delta=1e-6,
41+
min_mean_distance=1e-6,
42+
verbose=False,
43+
return_details=False,
44+
):
45+
46+
assert src_points.shape[1] == dst_points.shape[1]
47+
assert len(src_points.shape) == 2
48+
49+
def xprint(*args):
50+
if verbose:
51+
print(*args)
52+
53+
num_dim = src_points.shape[1]
54+
55+
R = initial_pose[:3, :3]
56+
t = initial_pose[:3, 3]
57+
src_points = src_points @ R.T + t
58+
59+
dst_kd_tree = _build_kd_tree(dst_points)
60+
nn_kwargs = {"kdtree_or_dst_points": dst_kd_tree, "dmax": max_distance}
61+
62+
distances = []
63+
src_closest_indices = []
64+
dst_closest_indices = []
65+
Rs = []
66+
ts = []
67+
68+
prog = utils.ProgressTimer(verbose=verbose)
69+
prog.tic(max_iterations)
70+
for _ in range(max_iterations):
71+
72+
src_indices, dst_indices = nearest_neighbors(src_points, **nn_kwargs)
73+
src_matched = src_points[src_indices]
74+
dst_matched = dst_points[dst_indices]
75+
76+
if len(src_indices) < num_dim:
77+
msg = f"ICP stopped early: only {len(src_indices)} "
78+
msg += "correspondences found."
79+
xprint(msg)
80+
break
81+
82+
# compute distance and check convergence
83+
d = np.sqrt(np.sum((src_matched - dst_matched) ** 2, axis=1))
84+
d = np.mean(d)
85+
86+
if d < min_mean_distance:
87+
msg = "ICP converged: "
88+
msg += f"mean distance {d:.6f} below threshold."
89+
xprint(msg)
90+
break
91+
92+
prev_d = distances[-1] if len(distances) > 0 else np.inf
93+
diff_d = np.abs(prev_d - d)
94+
if diff_d < min_delta:
95+
msg = "ICP converged: "
96+
msg += f"mean distance change {diff_d:.6f} below threshold."
97+
xprint(msg)
98+
break
99+
100+
# update src_points
101+
scale, R_delta, t_delta = utils_align.umeyama_alignment(
102+
src_matched,
103+
dst_matched,
104+
with_scale=False,
105+
)
106+
107+
src_points = (src_points @ R_delta.T) + t_delta
108+
109+
# update overall pose
110+
R = R_delta @ R
111+
t = R_delta @ t + t_delta
112+
113+
# store iteration details
114+
distances.append(d)
115+
src_closest_indices.append(src_indices)
116+
dst_closest_indices.append(dst_indices)
117+
Rs.append(R.copy())
118+
ts.append(t.copy())
119+
120+
prog.toc()
121+
122+
details = {
123+
"distances": distances,
124+
"src_closest_indices": src_closest_indices,
125+
"dst_closest_indices": dst_closest_indices,
126+
"Rs": Rs,
127+
"ts": ts,
128+
}
129+
130+
if return_details:
131+
return R, t, details
132+
133+
return R, t

src/py_utils/utils_align.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import numpy as np
2+
3+
4+
def umeyama_alignment(src, dst, with_scale=True):
5+
"""
6+
Perform Umeyama alignment to find s (scale), R (rotation), and
7+
t (translation) that align src to dst.
8+
dst = s * R * src + t
9+
"""
10+
11+
assert src.shape == dst.shape
12+
13+
mu_src = np.mean(src, axis=0)
14+
mu_dst = np.mean(dst, axis=0)
15+
16+
src_centered = src - mu_src
17+
dst_centered = dst - mu_dst
18+
19+
cov_matrix = dst_centered.T @ src_centered / src.shape[0]
20+
U, D, Vt = np.linalg.svd(cov_matrix)
21+
22+
R = U @ Vt
23+
# Ensure a proper rotation (determinant = 1)
24+
if np.linalg.det(R) < 0:
25+
Vt[-1, :] *= -1
26+
R = U @ Vt
27+
28+
if with_scale:
29+
var_src = np.var(src_centered, axis=0).sum()
30+
scale = np.sum(D) / var_src
31+
else:
32+
scale = 1.0
33+
34+
t = mu_dst - scale * R @ mu_src
35+
36+
return scale, R, t

0 commit comments

Comments
 (0)