-
Notifications
You must be signed in to change notification settings - Fork 11
/
icp.py
147 lines (115 loc) · 5.74 KB
/
icp.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
import math
import numpy as np
from sklearn.neighbors import NearestNeighbors
def euclidean_distance(point1, point2):
"""
Euclidean distance between two points.
:param point1: the first point as a tuple (a_1, a_2, ..., a_n)
:param point2: the second point as a tuple (b_1, b_2, ..., b_n)
:return: the Euclidean distance
"""
a = np.array(point1)
b = np.array(point2)
return np.linalg.norm(a - b, ord=2)
def point_based_matching(point_pairs):
"""
This function is based on the paper "Robot Pose Estimation in Unknown Environments by Matching 2D Range Scans"
by F. Lu and E. Milios.
:param point_pairs: the matched point pairs [((x1, y1), (x1', y1')), ..., ((xi, yi), (xi', yi')), ...]
:return: the rotation angle and the 2D translation (x, y) to be applied for matching the given pairs of points
"""
x_mean = 0
y_mean = 0
xp_mean = 0
yp_mean = 0
n = len(point_pairs)
if n == 0:
return None, None, None
for pair in point_pairs:
(x, y), (xp, yp) = pair
x_mean += x
y_mean += y
xp_mean += xp
yp_mean += yp
x_mean /= n
y_mean /= n
xp_mean /= n
yp_mean /= n
s_x_xp = 0
s_y_yp = 0
s_x_yp = 0
s_y_xp = 0
for pair in point_pairs:
(x, y), (xp, yp) = pair
s_x_xp += (x - x_mean)*(xp - xp_mean)
s_y_yp += (y - y_mean)*(yp - yp_mean)
s_x_yp += (x - x_mean)*(yp - yp_mean)
s_y_xp += (y - y_mean)*(xp - xp_mean)
rot_angle = math.atan2(s_x_yp - s_y_xp, s_x_xp + s_y_yp)
translation_x = xp_mean - (x_mean*math.cos(rot_angle) - y_mean*math.sin(rot_angle))
translation_y = yp_mean - (x_mean*math.sin(rot_angle) + y_mean*math.cos(rot_angle))
return rot_angle, translation_x, translation_y
def icp(reference_points, points, max_iterations=100, distance_threshold=0.3, convergence_translation_threshold=1e-3,
convergence_rotation_threshold=1e-4, point_pairs_threshold=10, verbose=False):
"""
An implementation of the Iterative Closest Point algorithm that matches a set of M 2D points to another set
of N 2D (reference) points.
:param reference_points: the reference point set as a numpy array (N x 2)
:param points: the point that should be aligned to the reference_points set as a numpy array (M x 2)
:param max_iterations: the maximum number of iteration to be executed
:param distance_threshold: the distance threshold between two points in order to be considered as a pair
:param convergence_translation_threshold: the threshold for the translation parameters (x and y) for the
transformation to be considered converged
:param convergence_rotation_threshold: the threshold for the rotation angle (in rad) for the transformation
to be considered converged
:param point_pairs_threshold: the minimum number of point pairs the should exist
:param verbose: whether to print informative messages about the process (default: False)
:return: the transformation history as a list of numpy arrays containing the rotation (R) and translation (T)
transformation in each iteration in the format [R | T] and the aligned points as a numpy array M x 2
"""
transformation_history = []
nbrs = NearestNeighbors(n_neighbors=1, algorithm='kd_tree').fit(reference_points)
for iter_num in range(max_iterations):
if verbose:
print('------ iteration', iter_num, '------')
closest_point_pairs = [] # list of point correspondences for closest point rule
distances, indices = nbrs.kneighbors(points)
for nn_index in range(len(distances)):
if distances[nn_index][0] < distance_threshold:
closest_point_pairs.append((points[nn_index], reference_points[indices[nn_index][0]]))
# if only few point pairs, stop process
if verbose:
print('number of pairs found:', len(closest_point_pairs))
if len(closest_point_pairs) < point_pairs_threshold:
if verbose:
print('No better solution can be found (very few point pairs)!')
break
# compute translation and rotation using point correspondences
closest_rot_angle, closest_translation_x, closest_translation_y = point_based_matching(closest_point_pairs)
if closest_rot_angle is not None:
if verbose:
print('Rotation:', math.degrees(closest_rot_angle), 'degrees')
print('Translation:', closest_translation_x, closest_translation_y)
if closest_rot_angle is None or closest_translation_x is None or closest_translation_y is None:
if verbose:
print('No better solution can be found!')
break
# transform 'points' (using the calculated rotation and translation)
c, s = math.cos(closest_rot_angle), math.sin(closest_rot_angle)
rot = np.array([[c, -s],
[s, c]])
aligned_points = np.dot(points, rot.T)
aligned_points[:, 0] += closest_translation_x
aligned_points[:, 1] += closest_translation_y
# update 'points' for the next iteration
points = aligned_points
# update transformation history
transformation_history.append(np.hstack((rot, np.array([[closest_translation_x], [closest_translation_y]]))))
# check convergence
if (abs(closest_rot_angle) < convergence_rotation_threshold) \
and (abs(closest_translation_x) < convergence_translation_threshold) \
and (abs(closest_translation_y) < convergence_translation_threshold):
if verbose:
print('Converged!')
break
return transformation_history, points