You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thanks for putting this code up! It's helped me get to understand ICP! In my implementation, I'm also attempting to estimate the scale between two point sets. One issue that has come up for me is the case where the correspondence between the closest points in the source and the target is not 1-to-1 via the nearest neighbour search. This may result in many points in the source set getting mapped to one (or a small number of points) in the target set or the converse, which then leads to an incorrect estimation of the transform (and what commonly happens is the scale parameter gets very small and the source points are all converging on one target point).
What I've done is I've allowed the user to specify to which "depth" they want to search for correspondences between point sets. See my code below:
fromsklearn.neighborsimportNearestNeighborsimportnumpyasnpdefmatch_points(source, target, max_correspondence_search=None):
ifmax_correspondence_searchisNone:
max_correspondence_search=source.shape[0]
# get the nearest neighbors up to some depth# the maximum depth gives the full distance matrix# this will guarantee a 1-to-1 correspondence between points by # distance, however it could be very slow for large datasetsnn=NearestNeighbors(n_neighbors=max_correspondence_search)
nn.fit(target)
distances, indicies=nn.kneighbors(source, return_distance=True)
# this will give us a list of the row and column indicies (in the distance matrix)# of the distances in increasing orderdist_argsort_row, dist_argsort_col=np.unravel_index(distances.ravel().argsort(), distances.shape)
source_idxs= []
target_idxs= []
dists= []
fordar, dacinzip(dist_argsort_row, dist_argsort_col):
ifdarnotinsource_idxs:
tidx=indicies[dar, dac]
iftidxnotintarget_idxs:
source_idxs.append(dar)
target_idxs.append(tidx)
dists.append(distances[dar, dac])
returnnp.array(dists), np.array(source_idxs), np.array(target_idxs)
defcompute_transform(source, target, return_as_single_matrix=False):
# basic equation we are trying to optimize# error = target - scale*Rot*source - translation# so we need to find the scale, rotation, and translation # that minimizes the above equation# based on notes from# https://igl.ethz.ch/projects/ARAP/svd_rot.pdf# made more clear from http://web.stanford.edu/class/cs273/refs/umeyama.pdf# in this implementation I assume that the matricies source and target are of # the form n x m, where n is the number of points in each matrix# and m is the number of dimensionsassertsource.shape==target.shapen, m=source.shape# compute centroidssource_centroid=source.mean(axis=0)
target_centroid=target.mean(axis=0)
# this removes the translation component of the transform# we can estimate the translation by the centroidssource_rel=source-source_centroidtarget_rel=target-target_centroidsource_var=source_rel.var()
# next we need to estimate the covariance matrix between# the source and target points (should be an mxm matrix)# in the literature this is often denoted as "H"H=target_rel.T.dot(source_rel) / (n*m)
# now we can do SVD on H to get the rotation matrixU, D, V=np.linalg.svd(H)
# rotation - first check the determinants of U and Vu_det=np.linalg.det(U)
v_det=np.linalg.det(V)
S=np.eye(m)
ifu_det*v_det<0.0:
S[-1] =-1rot=V.T.dot(S).dot(U)
# compute the scalescale= (np.trace(np.diag(D).dot(S)) /source_var)
# compute the translationtrans=target_centroid-source_centroid.dot(rot*scale)
ifreturn_as_single_matrix:
T=np.eye(m+1)
T[:m,:m] =scale*rotT[m,:m] =transreturnTreturnrot, trans, scaledeficp(source, target, max_iter=100, tol=1e-6, d_tol=1e-10, max_correspondence_search=None):
sn, sm=source.shapetn, tm=target.shapeassertsm==tm, "Dimensionality of point sets must be equal"S=source.copy()
T=target.copy()
# initialize the scale, rot, and translation estimates# here we will the respective centroids and scales get an initial correspondence between# the two point sets# if we just take the raw distances,# all the points in the source may map to one target and vice versaSc= ( (S-S.mean(axis=0)) /S.std(axis=0))
Tc= ( (T-T.mean(axis=0)) /T.std(axis=0))
d,s_idxs, t_idxs=match_points(Sc, Tc, max_correspondence_search=max_correspondence_search)
rotation, _, _=compute_transform( Sc[s_idxs, :], Tc[t_idxs, :] )
scale=1.0translation=T.mean(axis=0) -S.mean(axis=0).dot(scale*rotation)
S=S.dot(scale*rotation) +translationprev_err=1e6n_pt=0foriinrange(max_iter):
# match the closest points based on some distance metric (using the sklearn NearestNeighbor object)d,s_idxs,t_idxs=match_points(S, T, max_correspondence_search=max_correspondence_search)
# estimate the translation/rotation/scaling to match the source to the targetrotation, translation, scale=compute_transform(S[s_idxs, :], T[t_idxs, :])
# transform the source (i.e. update the positions of the source)S=S.dot(scale*rotation) +translation# repeat until convergence or max iterationserr=np.mean(d)
ifnp.abs(prev_err-err) <=tol:
breakprev_err=errrotation, translation, scale=compute_transform(source, S) # we know the exact correspondences herereturnrotation, translation, scale
I'm curious if you have come across any other methods for estimating the correspondences, or what to do when the algorithm gets stuck in a local minimum because the estimated correspondences aren't optimal but there is no improvement from iteration to iteration.
I've tested this code and it will perfectly recover the transform when max_correspondence_search is equal to the number of points in the source set of points (i.e. we are enforcing a one-to-one correspondence for the points). However, as you can imagine this isn't ideal for large point sets as the distance matrix that is being iterated over grows at an exponential rate (power of 2) as the number of points grows.
One way to improve this would be begin a more fine-grained search once the algorithm has hit the tolerance limit, and perhaps use a RANSAC-type approach, or some other method that breaks the data set into subsets to optimize the transform at the end. Just curious if you (or anyone else here) knows of some methods for this.
The text was updated successfully, but these errors were encountered:
Hi Clay,
Thanks for putting this code up! It's helped me get to understand ICP! In my implementation, I'm also attempting to estimate the scale between two point sets. One issue that has come up for me is the case where the correspondence between the closest points in the source and the target is not 1-to-1 via the nearest neighbour search. This may result in many points in the source set getting mapped to one (or a small number of points) in the target set or the converse, which then leads to an incorrect estimation of the transform (and what commonly happens is the scale parameter gets very small and the source points are all converging on one target point).
What I've done is I've allowed the user to specify to which "depth" they want to search for correspondences between point sets. See my code below:
I'm curious if you have come across any other methods for estimating the correspondences, or what to do when the algorithm gets stuck in a local minimum because the estimated correspondences aren't optimal but there is no improvement from iteration to iteration.
I've tested this code and it will perfectly recover the transform when
max_correspondence_search
is equal to the number of points in the source set of points (i.e. we are enforcing a one-to-one correspondence for the points). However, as you can imagine this isn't ideal for large point sets as the distance matrix that is being iterated over grows at an exponential rate (power of 2) as the number of points grows.One way to improve this would be begin a more fine-grained search once the algorithm has hit the tolerance limit, and perhaps use a RANSAC-type approach, or some other method that breaks the data set into subsets to optimize the transform at the end. Just curious if you (or anyone else here) knows of some methods for this.
The text was updated successfully, but these errors were encountered: