@@ -106,7 +106,7 @@ def xyt2tr(xyt, unit="rad"):
106
106
:type xyt: array_like(3)
107
107
:param unit: angular units: 'rad' [default], or 'deg'
108
108
:type unit: str
109
- :return: 3x3 homogeneous transformation matrix
109
+ :return: SE(2) matrix
110
110
:rtype: ndarray(3,3)
111
111
112
112
- ``xyt2tr([x,y,θ])`` is a homogeneous transformation (3x3) representing a rotation of
@@ -170,8 +170,8 @@ def transl2(x, y=None):
170
170
:type x: float
171
171
:param y: translation along Y-axis
172
172
:type y: float
173
- :return: SE(2) transform matrix or the translation elements of a homogeneous
174
- transform :rtype: ndarray(3,3)
173
+ :return: SE(2) matrix
174
+ :rtype: ndarray(3,3)
175
175
176
176
- ``T = transl2([X, Y])`` is an SE(2) homogeneous transform (3x3)
177
177
representing a pure translation.
@@ -584,14 +584,14 @@ def trinterp2(start, end, s=None):
584
584
:rtype: ndarray(3,3) or ndarray(2,2)
585
585
:raises ValueError: bad arguments
586
586
587
- - ``trinterp2(None, T, S)`` is a homogeneous transform (3x3) interpolated
588
- between identity when S =0 and T (3x3) when S =1.
587
+ - ``trinterp2(None, T, S)`` is an SE(2) matrix interpolated
588
+ between identity when `S` =0 and `T` when `S` =1.
589
589
- ``trinterp2(T0, T1, S)`` as above but interpolated
590
- between T0 (3x3) when S =0 and T1 (3x3) when S =1.
591
- - ``trinterp2(None, R, S)`` is a rotation matrix (2x2) interpolated
592
- between identity when S =0 and R (2x2) when S =1.
590
+ between `T0` when `S` =0 and `T1` when `S` =1.
591
+ - ``trinterp2(None, R, S)`` is an SO(2) matrix interpolated
592
+ between identity when `S` =0 and `R` when `S` =1.
593
593
- ``trinterp2(R0, R1, S)`` as above but interpolated
594
- between R0 (2x2) when S =0 and R1 (2x2) when S =1.
594
+ between `R0` when `S` =0 and `R1` when `S` =1.
595
595
596
596
.. note:: Rotation angle is linearly interpolated.
597
597
@@ -777,6 +777,160 @@ def points2tr2(p1, p2):
777
777
778
778
return T
779
779
780
+ # https://github.com/ClayFlannigan/icp/blob/master/icp.py
781
+ # https://github.com/1988kramer/intel_dataset/blob/master/scripts/Align2D.py
782
+ # hack below to use points2tr above
783
+ # use ClayFlannigan's improved data association
784
+ from scipy .spatial import KDTree
785
+ import numpy as np
786
+
787
+ # reference or target 2xN
788
+ # source 2xN
789
+
790
+ # params:
791
+ # source_points: numpy array containing points to align to the reference set
792
+ # points should be homogeneous, with one point per row
793
+ # reference_points: numpy array containing points to which the source points
794
+ # are to be aligned, points should be homogeneous with one
795
+ # point per row
796
+ # initial_T: initial estimate of the transform between reference and source
797
+ # def __init__(self, source_points, reference_points, initial_T):
798
+ # self.source = source_points
799
+ # self.reference = reference_points
800
+ # self.init_T = initial_T
801
+ # self.reference_tree = KDTree(reference_points[:,:2])
802
+ # self.transform = self.AlignICP(30, 1.0e-4)
803
+
804
+ # uses the iterative closest point algorithm to find the
805
+ # transformation between the source and reference point clouds
806
+ # that minimizes the sum of squared errors between nearest
807
+ # neighbors in the two point clouds
808
+ # params:
809
+ # max_iter: int, max number of iterations
810
+ # min_delta_err: float, minimum change in alignment error
811
+ def ICP2d (reference , source , T = None , max_iter = 20 , min_delta_err = 1e-4 ):
812
+
813
+ mean_sq_error = 1.0e6 # initialize error as large number
814
+ delta_err = 1.0e6 # change in error (used in stopping condition)
815
+ num_iter = 0 # number of iterations
816
+ if T is None :
817
+ T = np .eye (3 )
818
+
819
+ ref_kdtree = KDTree (reference .T )
820
+ tf_source = source
821
+
822
+ source_hom = np .vstack ((source , np .ones (source .shape [1 ])))
823
+
824
+ while delta_err > min_delta_err and num_iter < max_iter :
825
+
826
+ # find correspondences via nearest-neighbor search
827
+ matched_ref_pts , matched_source , indices = _FindCorrespondences (ref_kdtree , tf_source , reference )
828
+
829
+ # find alingment between source and corresponding reference points via SVD
830
+ # note: svd step doesn't use homogeneous points
831
+ new_T = _AlignSVD (matched_source , matched_ref_pts )
832
+
833
+ # update transformation between point sets
834
+ T = T @ new_T
835
+
836
+ # apply transformation to the source points
837
+ tf_source = T @ source_hom
838
+ tf_source = tf_source [:2 , :]
839
+
840
+ # find mean squared error between transformed source points and reference points
841
+ # TODO: do this with fancy indexing
842
+ new_err = 0
843
+ for i in range (len (indices )):
844
+ if indices [i ] != - 1 :
845
+ diff = tf_source [:, i ] - reference [:, indices [i ]]
846
+ new_err += np .dot (diff ,diff .T )
847
+
848
+ new_err /= float (len (matched_ref_pts ))
849
+
850
+ # update error and calculate delta error
851
+ delta_err = abs (mean_sq_error - new_err )
852
+ mean_sq_error = new_err
853
+ print ('ITER' , num_iter , delta_err , mean_sq_error )
854
+
855
+ num_iter += 1
856
+
857
+ return T
858
+
859
+
860
+ def _FindCorrespondences (tree , source , reference ):
861
+
862
+ # get distances to nearest neighbors and indices of nearest neighbors
863
+ dist , indices = tree .query (source .T )
864
+
865
+ # remove multiple associatons from index list
866
+ # only retain closest associations
867
+ unique = False
868
+ matched_src = source .copy ()
869
+ while not unique :
870
+ unique = True
871
+ for i , idxi in enumerate (indices ):
872
+ if idxi == - 1 :
873
+ continue
874
+ # could do this with np.nonzero
875
+ for j in range (i + 1 ,len (indices )):
876
+ if idxi == indices [j ]:
877
+ if dist [i ] < dist [j ]:
878
+ indices [j ] = - 1
879
+ else :
880
+ indices [i ] = - 1
881
+ break
882
+ # build array of nearest neighbor reference points
883
+ # and remove unmatched source points
884
+ point_list = []
885
+ src_idx = 0
886
+ for idx in indices :
887
+ if idx != - 1 :
888
+ point_list .append (reference [:,idx ])
889
+ src_idx += 1
890
+ else :
891
+ matched_src = np .delete (matched_src , src_idx , axis = 1 )
892
+
893
+ matched_ref = np .array (point_list ).T
894
+
895
+ return matched_ref , matched_src , indices
896
+
897
+ # uses singular value decomposition to find the
898
+ # transformation from the reference to the source point cloud
899
+ # assumes source and reference point clounds are ordered such that
900
+ # corresponding points are at the same indices in each array
901
+ #
902
+ # params:
903
+ # source: numpy array representing source pointcloud
904
+ # reference: numpy array representing reference pointcloud
905
+ # returns:
906
+ # T: transformation between the two point clouds
907
+
908
+ # TODO: replace this func with
909
+ def _AlignSVD (source , reference ):
910
+
911
+ # first find the centroids of both point clouds
912
+ src_centroid = source .mean (axis = 1 )
913
+ ref_centroid = reference .mean (axis = 1 )
914
+
915
+ # get the point clouds in reference to their centroids
916
+ source_centered = source - src_centroid [:, np .newaxis ]
917
+ reference_centered = reference - ref_centroid [:, np .newaxis ]
918
+
919
+ # compute the moment matrix
920
+ M = reference_centered @ source_centered .T
921
+
922
+ # do the singular value decomposition
923
+ U , W , V_t = np .linalg .svd (M )
924
+
925
+ # get rotation between the two point clouds
926
+ R = U @ V_t
927
+ if np .linalg .det (R ) < 0 :
928
+ raise RuntimeError ('bad rotation matrix' )
929
+
930
+ # translation is the difference between the point clound centroids
931
+ t = ref_centroid - R @ src_centroid
932
+
933
+ return base .rt2tr (R , t )
780
934
781
935
def trplot2 (
782
936
T ,
0 commit comments