Example #1
0
def calc_volume_closed(amp_in, return_closed=False):
    r"""
    Calculates the volume of a closed surface. If the surface is not closed the algorithm fills in the holes using a simple
    hole filling algorithm, the surface without holes can be accessed if return_closed is set to True.


    Parameters
    ----------
    amp: AmpObject 
        The AmpObject to analyse
    return_closed: bool, default False
        Indicate whether to return the shape with holes filled in  

    Returns
    -------
    vol: float
        The volume of the AmpObject
    amp: AmpObject
        If return_closed is True, then the closed shape is returned 
    """
    amp = AmpObject({
        'vert': amp_in.vert.copy(),
        'faces': amp_in.faces.copy(),
        'values': amp_in.values.copy(),
    })
    amp.calcStruct()
    # Fill in the holes
    while (amp.faceEdges == -99999).sum() != 0:
        # Find the edges which are only conected to one face
        edges = (amp.faceEdges == -99999).sum(axis=1).astype(bool)
        edges = amp.edges[edges, :]
        # Return the vert indicies for the loop
        vInd = logEuPath(edges)
        # Calculate the mmidpoint
        midpoint = amp.vert[vInd, :].mean(axis=0)
        # Add in the new vertex
        amp.vert = np.r_[amp.vert, midpoint[None, :]]
        f0 = amp.vert.shape[0] - 1
        # Add in each face using adjacent vertices in loop
        for f1, f2 in zip(vInd, np.roll(vInd, 1)):
            amp.faces = np.r_[amp.faces, [[f1, f0, f2]]]
        # Update structure and check if any more holes (algorithm keeps going until all holes filled)
        amp.calcStruct()
    # Calculate the area of each face in the array using vector cross product
    v01 = amp.vert[amp.faces[:, 1], :] - amp.vert[amp.faces[:, 0], :]
    v02 = amp.vert[amp.faces[:, 2], :] - amp.vert[amp.faces[:, 0], :]
    cp = np.square(np.cross(v01, v02))
    area = 0.5 * np.sqrt(cp.sum(axis=1))
    # Get surface volume contributions
    sVC = area * amp.vert[amp.faces, 2].mean(axis=1) * amp.norm[:, 2]
    if return_closed is True:
        return sVC.sum(), amp
    else:
        return sVC.sum()
Example #2
0
class registration(object):
    r"""
    Registration methods between two AmpObject meshes. This function morphs the baseline 
    vertices onto the surface of the target and returns a new AmpObject
    
    Parameters
    ----------
    baseline: AmpObject
    	The baseline AmpObject, the vertices from this will be morphed onto the target
    target: AmpObject
    	The target AmpObject, the shape that the baseline attempts to morph onto
    method: str: default 'point2plane'
    	A string of the method used for registration
    *args:
    	The arguments used for the registration methods
    **kwargs:
    	The keyword arguments used for the registration methods
        
    Returns
    -------
    reg: AmpObject
        The registered AmpObject, the vertices of this are on the surface of the target 
        and it has the same number of vertices and face array as the baseline AmpObject
        Access this accessing the registration.reg 
    
    Examples
    --------
    >>> from ampscan.core import AmpObject
    >>> baseline = AmpObject(basefh)
    >>> target = AmpObject(targfh)
    >>> reg = registration(baseline, target, steps=10, neigh=10, smooth=1).reg
		
    """ 
    def __init__(self, baseline, target, method='point2plane', *args, **kwargs):
        self.b = baseline
        self.t = target
        if method is not None:
            getattr(self, method)(*args, **kwargs)
        
        
    def point2plane(self, steps = 1, neigh = 10, inside = True, subset = None, 
                    scale=None, smooth=1, fixBrim=False, error='norm'):
        r"""
        Point to Plane method for registration between the two meshes 
        
        Parameters
        ----------
        steps: int, default 1
            Number of iterations
        int, default 10
            Number of nearest neighbours to interrogate for each baseline point
        inside: bool, default True
            If True, a barycentric centre check is made to ensure the registered 
            point lines within the target triangle
        subset: array_like, default None
            Indicies of the baseline nodes to include in the registration, default is none so 
            all are used
        scale: float, default None
            If not None scale the baseline mesh to match the target mesh in the z-direction, 
            the value of scale will be used as a plane from which the nodes are scaled.
            Nodes with a higher z value will not be scaled. 
        smooth: int, default 1
            Indicate number of laplacian smooth steps in between the steps 
        fixBrim: bool, default False
            If True, the nodes on the brim line will not be included in the smooth
        error: bool, default False
            If True, the polarity will be included when calculating the distance 
            between the target and baseline mesh
		
        """
        # Calc FaceCentroids
        fC = self.t.vert[self.t.faces].mean(axis=1)
        # Construct knn tree
        tTree = spatial.cKDTree(fC)
        bData = dict(zip(['vert', 'faces', 'values'], 
                         [self.b.vert, self.b.faces, self.b.values]))
        regData = copy.deepcopy(bData)
        self.reg = AmpObject(regData, stype='reg')
        self.disp = AmpObject({'vert': np.zeros(self.reg.vert.shape),
                               'faces': self.reg.faces,
                               'values':self.reg.values})
        if scale is not None:
            tmin = self.t.vert.min(axis=0)[2]
            rmin = self.reg.vert.min(axis=0)[2]
            SF = ((tmin-scale)/(rmin-scale)) - 1
            logic = self.reg.vert[:, 2] < scale
            d = (self.reg.vert[logic, 2] - scale) * SF
            self.disp.vert[logic, 2] += d
            self.reg.vert = self.b.vert + self.disp.vert
        normals = np.cross(self.t.vert[self.t.faces[:,1]] -
                         self.t.vert[self.t.faces[:,0]],
                         self.t.vert[self.t.faces[:,2]] -
                         self.t.vert[self.t.faces[:,0]])
        mag = (normals**2).sum(axis=1)
        for step in np.arange(steps, 0, -1, dtype=float):
            # Index of 10 centroids nearest to each baseline vertex
            ind = tTree.query(self.reg.vert, neigh)[1]
            # Define normals for faces of nearest faces
            norms = normals[ind]
            # Get a point on each face
            fPoints = self.t.vert[self.t.faces[ind, 0]]
            # Calculate dot product between point on face and normals
            d = np.einsum('ijk, ijk->ij', norms, fPoints)
            t = (d - np.einsum('ijk, ik->ij', norms, self.reg.vert))/mag[ind]
            # Calculate the vector from old point to new point
            G = self.reg.vert[:, None, :] + np.einsum('ijk, ij->ijk', norms, t)
            # Ensure new points lie inside points otherwise set to 99999
            # Find smallest distance from old to new point 
            if inside is False:
                G = G - self.reg.vert[:, None, :]
                GMag = np.sqrt(np.einsum('ijk, ijk->ij', G, G))
                GInd = GMag.argmin(axis=1)
            else:
                G, GInd = self.__calcBarycentric(self.reg.vert, G, ind)
            # Define vector from baseline point to intersect point
            D = G[np.arange(len(G)), GInd, :]
#            rVert += D/step
            self.disp.vert += D/step
            if smooth > 0 and step > 1:
                self.disp.lp_smooth(smooth, brim = fixBrim)
                self.reg.vert = self.b.vert + self.disp.vert
            else:
                self.reg.vert = self.b.vert + self.disp.vert
                self.reg.calcNorm()
        self.reg.calcStruct(vNorm=True)
        self.reg.values[:] = self.calcError(error)
        
    def calcError(self, method='norm'):
        r"""
        Calculate the magnitude of distances between the baseline and registered array
		
        Parameters
        ----------
        method: str, default 'norm'
            The method used to calculate the distances. 'abs' returns the absolute
            distance. 'cent'calculates polarity based upon distance from centroid.
            'norm' calculates dot product between baseline vertex normal and distance 
            normal

        Returns
        -------
        values: array_like
            Magnitude of distances

        """
        method = '_registration__' + method + 'Dist'
        try:
            values = getattr(self, method)()
            return values
        except: ValueError('"%s" is not a method, try "abs", "cent" or "prod"' % method)
        

    
    def __absDist(self):
        r"""
        Return the error based upon the absolute distance
        
        Returns
        -------
        values: array_like
            Magnitude of distances

        """
        return np.linalg.norm(self.reg.vert - self.b.vert, axis=1)
    
    def __centDist(self):
        r"""
        Return the error based upon distance from centroid 
        
        Returns
        -------
        values: array_like
            Magnitude of distances

        """
        values = np.linalg.norm(self.reg.vert - self.b.vert, axis=1)
        cent = self.b.vert.mean(axis=0)
        r = np.linalg.norm(self.reg.vert - cent, axis=1)
        b = np.linalg.norm(self.b.vert - cent, axis=1)
        polarity = np.ones([self.reg.vert.shape[0]])
        polarity[r<b] = -1
        return values * polarity

    def __normDist(self):
        r"""
        Returns error based upon scalar product of normal 
        
        Returns
        -------
        values: array_like
            Magnitude of distances

        """
        self.b.calcVNorm()
        D = self.reg.vert - self.b.vert
        n = self.b.vNorm
        values = np.linalg.norm(D, axis=1)
        polarity = np.sum(n*D, axis=1) < 0
        values[polarity] *= -1.0
        return values
        
        
    def __calcBarycentric(self, vert, G, ind):
        r"""
        Calculate the barycentric co-ordinates of each target face and the registered vertex, 
        this ensures that the registered vertex is within the bounds of the target face. If not 
        the registered vertex is moved to the nearest vertex on the target face 

        Parameters
        ----------
        vert: array_like
            The array of baseline vertices
        G: array_like
            The array of candidates for registered vertices. If neigh>1 then axis 2 will correspond 
            to the number of nearest neighbours selected
        ind: array_like
            The index of the nearest faces to the baseline vertices
        
        Returns
        -------
        G: array_like 
            The new array of candidates for registered vertices, from here, the one with 
            smallest magnitude is selected. All these points will lie within the target face
        GInd: array_like
            The index of the shortest distance between each baseline vertex and the registered vertex
            
        """
        P0 = self.t.vert[self.t.faces[ind, 0]]
        P1 = self.t.vert[self.t.faces[ind, 1]]
        P2 = self.t.vert[self.t.faces[ind, 2]]
        
        v0 = P2 - P0
        v1 = P1 - P0
        v2 = G - P0
        
        d00 = np.einsum('ijk, ijk->ij', v0, v0)
        d01 = np.einsum('ijk, ijk->ij', v0, v1)
        d02 = np.einsum('ijk, ijk->ij', v0, v2)
        d11 = np.einsum('ijk, ijk->ij', v1, v1)
        d12 = np.einsum('ijk, ijk->ij', v1, v2)
        
        denom = d00*d11 - d01*d01
        u = (d11 * d02 - d01 * d12)/denom
        v = (d00 * d12 - d01 * d02)/denom
        # Test if inside 
        logic = (u >= 0) * (v >= 0) * (u + v < 1)
        
        P = np.stack([P0, P1, P2], axis=3)
        pg = G[:, :, :, None] - P
        pd =  np.linalg.norm(pg, axis=2)
        pdx = pd.argmin(axis=2)
        i, j = np.meshgrid(np.arange(P.shape[0]), np.arange(P.shape[1]))
        nearP = P[i.T, j.T, :, pdx]
        G[~logic, :] = nearP[~logic, :]
        G = G - vert[:, None, :]
        GMag = np.sqrt(np.einsum('ijk, ijk->ij', G, G))
        GInd = GMag.argmin(axis=1)
        return G, GInd
    
    def plotResults(self, name=None, xrange=None, color=None, alpha=None):
        r"""
        Function to generate a mpl figure. Includes a rendering of the 
        AmpObject, a histogram of the registration values 
        
        Returns
        -------
        fig: mplfigure
            A matplot figure of the standard analysis
        
        """
        fig, ax = plt.subplots(1)
        n, bins, _ = ax.hist(self.reg.values, 50, density=True, range=xrange,
                             color=color, alpha=alpha)
        mean = self.reg.values.mean()
        stdev = self.reg.values.std()
        ax.set_title(r'Distribution of shape variance, '
                     '$\mu=%.2f$, $\sigma=%.2f$' % (mean, stdev))
        ax.set_xlim(None)
        if name is not None:
            plt.savefig(name, dpi = 300)
        return ax, n, bins