From a1defa6c9276f3a0deb64579297539c7af7aac72 Mon Sep 17 00:00:00 2001
From: Joshua Steer <joshua.w.steer@gmail.com>
Date: Mon, 27 Aug 2018 18:26:11 +0100
Subject: [PATCH] Added minimisation algorithms into the ICP algorithm

---
 AmpScan/align.py   | 369 ++++++++++++++++++++++++++++++---------------
 AmpScan/core.py    |   4 +-
 GUIs/AmpScanGUI.py |   2 +-
 3 files changed, 249 insertions(+), 126 deletions(-)

diff --git a/AmpScan/align.py b/AmpScan/align.py
index 286d0c4..0864064 100644
--- a/AmpScan/align.py
+++ b/AmpScan/align.py
@@ -1,8 +1,7 @@
 # -*- coding: utf-8 -*-
 """
-Created on Thu Sep 14 13:15:30 2017
-
-@author: js22g12
+Package for dealing with alignment methods between two AmpObject meshes
+Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk
 """
 
 import numpy as np
@@ -17,134 +16,67 @@ from .ampVis import vtkRenWin
 
 class align(object):
     r"""
-    Using this function for sample docstring (one line desc).
-
-    A more extended description that provides details of how the function works.
-
+    Automated alignment methods between two meshes
+    
     Parameters
     ----------
-    moving : array_like
-        Rot has a structure that can be iterated through implying it should be an
-        array like structure
-    static : data_type
-        Description of tTree input and what it does.
-    method : data_type
-        Desc of method
+    moving: AmpObject
+        The moving AmpObject that is to be aligned to the static object
+    static: AmpObject
+        The static AmpObject that the moving AmpObject that the moving object 
+        will be aligned to
+    method: str, default 'linPoint2Plane'
+        A string of the method used for alignment
+    *args:
+    	The arguments used for the registration methods
+    **kwargs:
+    	The keyword arguments used for the registration methods
 
     Returns
     -------
-    type
-        Explanation of anonymous return value of type ``type``.
-    describe : type
-        Explanation of return value named `describe`.
-    out : type
-        Explanation of `out`.
-    type_without_description
-
-    Other Parameters
-    ----------------
-    only_seldom_used_keywords : type
-        Explanation
-    common_parameters_listed_above : type
-        Explanation
-
-    Raises
-    ------
-    BadException
-        Because you shouldn't have done that.
-
-    See Also
-    --------
-    otherfunc : relationship (optional)
-    newfunc : Relationship (optional), which could be fairly long, in which
-              case the line wraps here.
-    thirdfunc, fourthfunc, fifthfunc
-
-    Notes
-    -----
-    Notes about the implementation algorithm (if needed).
-
-    This can have multiple paragraphs.
-
-    You may include some math:
-
-    .. math:: X(e^{j\omega } ) = x(n)e^{ - j\omega n}
-
-    And even use a Greek symbol like :math:`\omega` inline.
-
-    References
-    ----------
-    Cite the relevant literature, e.g. [1]_.  You may also cite these
-    references in the notes section above.
-
-    .. [1] O. McNoleg, "The integration of GIS, remote sensing,
-       expert systems and adaptive co-kriging for environmental habitat
-       modelling of the Highland Haggis using object-oriented, fuzzy-logic
-       and neural-network techniques," Computers & Geosciences, vol. 22,
-       pp. 585-588, 1996.
+    m: AmpObject
+        The aligned AmpObject, it same number of vertices and face array as 
+        the moving AmpObject
+        Access this using align.m
 
     Examples
     --------
-    These are written in doctest format, and should illustrate how to
-    use the function.
-
-    >>> a = [1, 2, 3]
-    >>> print [x + 3 for x in a]
-    [4, 5, 6]
-    >>> print "a\n\nb"
-    a
-    b
+    >>> static = AmpScan.AmpObject(staticfh)
+    >>> moving = AmpScan.AmpObject(movingfh)
+    >>> al = AmpScan.align(moving, static).m
 
     """    
 
-    def __init__(self, moving, static, method = 'linearICP', *args, **kwargs):
+    def __init__(self, moving, static, method = 'linPoint2Plane', *args, **kwargs):
         mData = dict(zip(['vert', 'faces', 'values'], 
                          [moving.vert, moving.faces, moving.values]))
         alData = copy.deepcopy(mData)
         self.m = AmpObject(alData, stype='reg')
         self.s = static
-        if method is not None:
-            getattr(self, method)(*args, **kwargs)
+        self.runICP(method=method, *args, **kwargs)
         
-    def icp(self):
+    
+    def runICP(self, method = 'linPoint2Plane', maxiter=20, inlier=1.0,
+               initTransform=None, *args, **kwargs):
         r"""
-        Automated alignment function between two meshes
+        The function to run the ICP algorithm, this function calls one of 
+        multiple methods to calculate the affine transformation 
         
-        """
-
-        tTree = spatial.cKDTree(self.s.vert)
-        rot = np.array([0,0,0], dtype=float)
-        res = minimize(self.calcDistError, rot, method='BFGS',
-                       options={'gtol':1e-6, 'disp':True})
+        Parameters
+        ----------
+        method: str, default 'linPoint2Plane'
+            A string of the method used for alignment
+        maxiter: int, default 20
+            Maximum number of iterations to run the ICP algorithm
+        inlier: float, default 1.0
+            The proportion of closest points to use to calculate the 
+            transformation, if < 1 then vertices with highest error are 
+            discounted
+        *args:
+        	The arguments used for the registration methods
+        **kwargs:
+        	The keyword arguments used for the registration methods
         
-        
-    def calcDistError(self, rot, tTree):
-        r"""
-        Needs description. Note the blank line at the end of each 
-        docstring - this is necessary.
-
-        """
-        Id = np.identity(3)
-        for i in range(3):
-            if rot[i] != 0:
-                ax = Id[i, :]
-                ang = np.deg2rad(rot[i])
-                dot = np.reshape(self.vert[:, 0] * ax[0] +
-                                 self.vert[:, 1] * ax[1] +
-                                 self.vert[:, 2] * ax[2], (-1, 1))
-                self.vert = (self.vert * np.cos(ang) +
-                             np.cross(ax, self.vert) * np.sin(ang) +
-                             np.reshape(ax, (1, -1)) * dot * (1-np.cos(ang)))
-        dist = tTree.query(self.vert, 10)[0]
-        dist = dist.min(axis=1)
-        return dist.sum()
-    
-    def linearICP(self, metric = 'point2point', 
-                  maxiter=20, inlier=1.0, initTransform=None):
-        """
-        Iterative Closest Point algorithm which relies on using least squares
-        method on a having made the minimisation problem into a set of linear 
-        equations
         """
         # Define the rotation, translation, error and quaterion arrays
         Rs = np.zeros([3, 3, maxiter+1])
@@ -152,7 +84,7 @@ class align(object):
 #        qs = np.r_[np.ones([1, maxiter+1]), 
 #                   np.zeros([6, maxiter+1])]
 #        dq  = np.zeros([7, maxiter+1])
-        dTheta = np.zeros([maxiter+1])
+#        dTheta = np.zeros([maxiter+1])
         err = np.zeros([maxiter+1])
         if initTransform is None:
             initTransform = np.eye(4)
@@ -173,14 +105,20 @@ class align(object):
         [dist, idx, sort] = dist[:inlier], idx[:inlier], sort[:inlier]
         err[0] = math.sqrt(dist.mean())
         for i in range(maxiter):
-            if metric == 'point2point':
-                [R, T] = getattr(self, metric)(self.m.vert[sort],
-                                               fC[idx, :])
-            else: 
-                [R, T] = getattr(self, metric)(self.m.vert[sort],
+            if method == 'linPoint2Point':
+                [R, T] = getattr(self, method)(self.m.vert[sort, :],
+                                               fC[idx, :], 
+                                               *args, **kwargs)
+            elif method == 'linPoint2Plane': 
+                [R, T] = getattr(self, method)(self.m.vert[sort, :],
                                                fC[idx, :], 
-                                               self.s.norm[idx, :])
-                
+                                               self.s.norm[idx, :],
+                                               *args, **kwargs)
+            elif method == 'optPoint2Point':
+                [R, T] = getattr(self, method)(self.m.vert[sort, :],
+                                               fC[idx, :],
+                                               *args, **kwargs)
+            else: KeyError('Not a supported alignment method')
             Rs[:, :, i+1] = np.dot(R, Rs[:, :, i])
             Ts[:, i+1] = np.dot(R, Ts[:, i]) + T
             self.m.rigidTransform(R, T)
@@ -202,7 +140,47 @@ class align(object):
             
     
     @staticmethod
-    def point2plane(mv, sv, sn):
+    def linPoint2Plane(mv, sv, sn):
+        r"""
+        Iterative Closest Point algorithm which relies on using least squares
+        method from converting the minimisation problem into a set of linear 
+        equations. This uses a 
+        
+        Parameters
+        ----------
+        mv: ndarray
+            The array of vertices to be moved 
+        sv: ndarray
+            The array of static vertices, these are the face centroids of the 
+            static mesh
+        sn: ndarray
+            The normals of the point in teh static array, these are derived 
+            from the normals of the faces for each centroid
+        
+        Returns
+        -------
+        R: ndarray
+            The optimal rotation array 
+        T: ndarray
+            The optimal translation array
+        
+        References
+        ----------
+        .. [1] Besl, Paul J.; N.D. McKay (1992). "A Method for Registration of 3-D
+           Shapes". IEEE Trans. on Pattern Analysis and Machine Intelligence (Los
+           Alamitos, CA, USA: IEEE Computer Society) 14 (2): 239-256.
+        
+        .. [2] Chen, Yang; Gerard Medioni (1991). "Object modelling by registration of
+           multiple range images". Image Vision Comput. (Newton, MA, USA:
+           Butterworth-Heinemann): 145-155
+
+        Examples
+        --------
+        >>> static = AmpScan.AmpObject(staticfh)
+        >>> moving = AmpScan.AmpObject(movingfh)
+        >>> al = AmpScan.align(moving, static, method='linPoint2Plane').m
+        
+        """
         cn = np.c_[np.cross(mv, sn), sn]
         C = np.dot(cn.T, cn)
         v = sv - mv
@@ -219,7 +197,43 @@ class align(object):
         return (R, T)
     
     @staticmethod
-    def point2point(mv, sv):
+    def linPoint2Point(mv, sv):
+        r"""
+        Point-to-Point Iterative Closest Point algorithm which 
+        relies on using singular value decomposition on the centered arrays.  
+        
+        Parameters
+        ----------
+        mv: ndarray
+            The array of vertices to be moved 
+        sv: ndarray
+            The array of static vertices, these are the face centroids of the 
+            static mesh
+        
+        Returns
+        -------
+        R: ndarray
+            The optimal rotation array 
+        T: ndarray
+            The optimal translation array
+        
+        References
+        ----------
+        .. [1] Besl, Paul J.; N.D. McKay (1992). "A Method for Registration of 3-D
+           Shapes". IEEE Trans. on Pattern Analysis and Machine Intelligence (Los
+           Alamitos, CA, USA: IEEE Computer Society) 14 (2): 239-256.
+        
+        .. [2] Chen, Yang; Gerard Medioni (1991). "Object modelling by registration of
+           multiple range images". Image Vision Comput. (Newton, MA, USA:
+           Butterworth-Heinemann): 145-155
+
+        Examples
+        --------
+        >>> static = AmpScan.AmpObject(staticfh)
+        >>> moving = AmpScan.AmpObject(movingfh)
+        >>> al = AmpScan.align(moving, static, method='linPoint2Point').m
+
+        """
         mCent = mv - mv.mean(axis=0)
         sCent = sv - sv.mean(axis=0)
         C = np.dot(mCent.T, sCent)
@@ -231,10 +245,119 @@ class align(object):
         R = np.dot(R, U.T)
         T = sv.mean(axis=0) - np.dot(R, mv.mean(axis=0))
         return (R, T)
+    
+    @staticmethod
+    def optPoint2Point(mv, sv, opt='L-BFGS-B'):
+        r"""
+        Direct minimisation of the rmse between the points of the two meshes. This 
+        method enables access to all of Scipy's minimisation algorithms 
+        
+        Parameters
+        ----------
+        mv: ndarray
+            The array of vertices to be moved 
+        sv: ndarray
+            The array of static vertices, these are the face centroids of the 
+            static mesh
+        opt: str, default 'L_BFGS-B'
+            The string of the scipy optimiser to use 
         
+        Returns
+        -------
+        R: ndarray
+            The optimal rotation array 
+        T: ndarray
+            The optimal translation array
+            
+        Examples
+        --------
+        >>> static = AmpScan.AmpObject(staticfh)
+        >>> moving = AmpScan.AmpObject(movingfh)
+        >>> al = AmpScan.align(moving, static, method='optPoint2Point', opt='SLSQP').m
+            
+        """
+        X = np.zeros(6)
+        lim = [-np.pi/4, np.pi/4] * 3 + [-5, 5] * 3
+        lim = np.reshape(lim, [6, 2])
+        try:
+            X = minimize(align.optDistError, X,
+                         args=(mv, sv),
+                         bounds=lim, method=opt)
+        except:
+            X = minimize(align.optDistError, X,
+                         args=(mv, sv),
+                         method=opt)
+        [angx, angy, angz] = X.x[:3]
+        Rx = np.array([[1, 0, 0],
+                       [0, np.cos(angx), -np.sin(angx)],
+                       [0, np.sin(angx), np.cos(angx)]])
+        Ry = np.array([[np.cos(angy), 0, np.sin(angy)],
+                       [0, 1, 0],
+                       [-np.sin(angy), 0, np.cos(angy)]])
+        Rz = np.array([[np.cos(angz), -np.sin(angz), 0],
+                       [np.sin(angz), np.cos(angz), 0],
+                       [0, 0, 1]])
+        R = np.dot(np.dot(Rz, Ry), Rx)
+        T = X.x[3:]
+        return (R, T)
+
+    @staticmethod
+    def optDistError(X, mv, sv):
+        r"""
+        The function to minimise. It performs the affine transformation then returns 
+        the rmse between the two vertex sets
+        
+        Parameters
+        ----------
+        X:  ndarray
+            The affine transformation corresponding to [Rx, Ry, Rz, Tx, Ty, Tz]
+        mv: ndarray
+            The array of vertices to be moved 
+        sv: ndarray
+            The array of static vertices, these are the face centroids of the 
+            static mesh
+
+        Returns
+        -------
+        err: float
+            The RMSE between the two meshes
+        
+        """
+        [angx, angy, angz] = X[:3]
+        Rx = np.array([[1, 0, 0],
+                       [0, np.cos(angx), -np.sin(angx)],
+                       [0, np.sin(angx), np.cos(angx)]])
+        Ry = np.array([[np.cos(angy), 0, np.sin(angy)],
+                       [0, 1, 0],
+                       [-np.sin(angy), 0, np.cos(angy)]])
+        Rz = np.array([[np.cos(angz), -np.sin(angz), 0],
+                       [np.sin(angz), np.cos(angz), 0],
+                       [0, 0, 1]])
+        R = np.dot(np.dot(Rz, Ry), Rx)
+        moved = np.dot(mv, R.T)
+        moved += X[3:]
+        dist = (moved - sv)**2
+        dist = dist.sum(axis=1)
+        err = np.sqrt(dist.mean())
+        return err
+
     
     @staticmethod
     def rot2quat(R):
+        """
+        Convert a rotation matrix to a quaternionic matrix
+        
+        Parameters
+        ----------
+        R: array_like
+            The 3x3 rotation array to be converted to a quaternionic matrix
+        
+        Returns
+        -------
+        Q: ndarray
+            The quaternionic matrix
+
+        """
         [[Qxx, Qxy, Qxz],
          [Qyx, Qyy, Qyz],
          [Qzx, Qzy, Qzz]] = R
@@ -273,7 +396,9 @@ class align(object):
     
     def display(self):
         r"""
-        Function to display the two aligned meshes in 
+        Display the static mesh and the aligned within an interactive VTK 
+        window 
+        
         """
         if not hasattr(self.s, 'actor'):
             self.s.addActor()
diff --git a/AmpScan/core.py b/AmpScan/core.py
index e85440f..b26816f 100644
--- a/AmpScan/core.py
+++ b/AmpScan/core.py
@@ -401,9 +401,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin,
         """
         if ang == 'deg':
             rot = np.deg2rad(rot)
-        angx = rot[0]
-        angy = rot[1]
-        angz = rot[2]
+        [angx, angy, angz] = rot
         Rx = np.array([[1, 0, 0],
                        [0, np.cos(angx), -np.sin(angx)],
                        [0, np.sin(angx), np.cos(angx)]])
diff --git a/GUIs/AmpScanGUI.py b/GUIs/AmpScanGUI.py
index 9071891..0c6d06c 100644
--- a/GUIs/AmpScanGUI.py
+++ b/GUIs/AmpScanGUI.py
@@ -125,7 +125,7 @@ class AmpScanGUI(QMainWindow):
         self.fileManager.setTable(static, [1,0,0], 0.5, 2)
         self.fileManager.setTable(moving, [0,0,1], 0.5, 2)
         al = align(self.files[moving], self.files[static], 
-                   maxiter=10, method='linearICP',metric='point2plane').m
+                   maxiter=10, method='linPoint2Plane').m
         al.addActor()
         alName = moving + '_al'
         self.files[alName] = al
-- 
GitLab