Commit 2c63b0e8 authored by jp6g18's avatar jp6g18
Browse files

Merge branch 'master' into 'Omar'

# Conflicts:
#   AmpScan/ampVis.py
#   GUIs/AmpScanGUI.py
parents 5a608740 13398e31
Pipeline #884 failed with stage
in 25 seconds
sample_job: #doctests and unitests:
script: python tests/sample_test.py # script: pytest --doctest-modules -v --ignore=GUIs
# Temporarily added back old testing
unittests:
script: python -m unittest discover tests -v
core doctests:
script: python -m doctest -v AmpScan/core.py
registration doctests:
script: python -m doctest -v AmpScan/registration.py
align doctests:
script: python -m doctest -v AmpScan/align.py
...@@ -10,8 +10,13 @@ import vtk ...@@ -10,8 +10,13 @@ import vtk
import math import math
from scipy import spatial from scipy import spatial
from scipy.optimize import minimize from scipy.optimize import minimize
from .core import AmpObject from AmpScan.core import AmpObject
from .ampVis import vtkRenWin from AmpScan.ampVis import vtkRenWin
# For doc examples
import os
staticfh = os.getcwd() + "\\tests\\stl_file.stl"
movingfh = os.getcwd() + "\\tests\\stl_file_2.stl"
class align(object): class align(object):
...@@ -41,9 +46,9 @@ class align(object): ...@@ -41,9 +46,9 @@ class align(object):
Examples Examples
-------- --------
>>> static = AmpScan.AmpObject(staticfh) >>> static = AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh) >>> moving = AmpObject(movingfh)
>>> al = AmpScan.align(moving, static).m >>> al = align(moving, static).m
""" """
...@@ -131,7 +136,7 @@ class align(object): ...@@ -131,7 +136,7 @@ class align(object):
R = Rs[:, :, -1] R = Rs[:, :, -1]
#Simpl #Simpl
[U, s, V] = np.linalg.svd(R) [U, s, V] = np.linalg.svd(R)
R = np.dot(U, V.T) R = np.dot(U, V)
self.tForm = np.r_[np.c_[R, np.zeros(3)], np.append(Ts[:, -1], 1)[:, None].T] self.tForm = np.r_[np.c_[R, np.zeros(3)], np.append(Ts[:, -1], 1)[:, None].T]
self.R = R self.R = R
self.T = Ts[:, -1] self.T = Ts[:, -1]
...@@ -176,9 +181,9 @@ class align(object): ...@@ -176,9 +181,9 @@ class align(object):
Examples Examples
-------- --------
>>> static = AmpScan.AmpObject(staticfh) >>> static = AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh) >>> moving = AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='linPoint2Plane').m >>> al = align(moving, static, method='linPoint2Plane').m
""" """
cn = np.c_[np.cross(mv, sn), sn] cn = np.c_[np.cross(mv, sn), sn]
...@@ -229,9 +234,9 @@ class align(object): ...@@ -229,9 +234,9 @@ class align(object):
Examples Examples
-------- --------
>>> static = AmpScan.AmpObject(staticfh) >>> static = AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh) >>> moving = AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='linPoint2Point').m >>> al = align(moving, static, method='linPoint2Point').m
""" """
mCent = mv - mv.mean(axis=0) mCent = mv - mv.mean(axis=0)
...@@ -271,9 +276,9 @@ class align(object): ...@@ -271,9 +276,9 @@ class align(object):
Examples Examples
-------- --------
>>> static = AmpScan.AmpObject(staticfh) >>> static = AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh) >>> moving = AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='optPoint2Point', opt='SLSQP').m >>> al = align(moving, static, method='optPoint2Point', opt='SLSQP').m
""" """
X = np.zeros(6) X = np.zeros(6)
......
...@@ -374,7 +374,7 @@ class visMixin(object): ...@@ -374,7 +374,7 @@ class visMixin(object):
def genIm(self, size=[512, 512], views=[[0, -1, 0]], def genIm(self, size=[512, 512], views=[[0, -1, 0]],
background=[1.0, 1.0, 1.0], projection=True, background=[1.0, 1.0, 1.0], projection=True,
shading=True, mag=10, out='im', fh='test.tiff', shading=True, mag=10, out='im', fh='test.tiff',
zoom=1.0, az = 0, el=0,crop=False): zoom=1.0, az = 0, el=0,crop=False, cam=None):
r""" r"""
Creates a temporary off screen vtkRenWin which is then either returned Creates a temporary off screen vtkRenWin which is then either returned
as a numpy array or saved as a .png file as a numpy array or saved as a .png file
...@@ -411,6 +411,7 @@ class visMixin(object): ...@@ -411,6 +411,7 @@ class visMixin(object):
self.addActor() self.addActor()
# Generate a renderer window # Generate a renderer window
win = vtkRenWin() win = vtkRenWin()
win.OffScreenRenderingOn()
# Set the number of viewports # Set the number of viewports
win.setnumViewports(len(views)) win.setnumViewports(len(views))
# Set the background colour # Set the background colour
...@@ -427,7 +428,8 @@ class visMixin(object): ...@@ -427,7 +428,8 @@ class visMixin(object):
# win.setProjection(projection, viewport=i) # win.setProjection(projection, viewport=i)
win.renderActors([self.actor,], zoom=zoom) win.renderActors([self.actor,], zoom=zoom)
win.rens[0].GetActiveCamera().Azimuth(az) win.rens[0].GetActiveCamera().Azimuth(az)
win.rens[0].GetActiveCamera().Elevation(el) if cam is not None:
win.rens[0].SetActiveCamera(cam)
win.Render() win.Render()
if out == 'im': if out == 'im':
im = win.getImage() im = win.getImage()
...@@ -438,7 +440,7 @@ class visMixin(object): ...@@ -438,7 +440,7 @@ class visMixin(object):
mask = np.all(im == 1, axis=2) mask = np.all(im == 1, axis=2)
mask = ~np.all(mask, axis=0) mask = ~np.all(mask, axis=0)
im = im[:, mask, :] im = im[:, mask, :]
return im return im, win
elif out == 'fh': elif out == 'fh':
win.getScreenshot(fh) win.getScreenshot(fh)
return return
......
...@@ -6,11 +6,17 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk ...@@ -6,11 +6,17 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk
""" """
import numpy as np import numpy as np
import os
import struct import struct
from .trim import trimMixin from AmpScan.trim import trimMixin
from .smooth import smoothMixin from AmpScan.smooth import smoothMixin
from .analyse import analyseMixin from AmpScan.analyse import analyseMixin
from .ampVis import visMixin from AmpScan.ampVis import visMixin
# The file path used in doc examples
filename = os.getcwd()+"\\tests\\stl_file.stl"
class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
r""" r"""
...@@ -36,8 +42,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -36,8 +42,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples Examples
------- -------
>>> fh = 'test.stl' >>> amp = AmpObject(filename)
>>> amp = AmpScan.AmpObject(fh)
""" """
...@@ -149,13 +154,12 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -149,13 +154,12 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples Examples
-------- --------
>>> fh = 'test.stl' >>> amp = AmpObject(filename, unify=False)
>>> amp = AmpObject(fh, unify=False)
>>> amp.vert.shape >>> amp.vert.shape
(600, 3) (44832, 3)
>>> amp.unifyVert() >>> amp.unifyVert()
>>> amp.vert.shape >>> amp.vert.shape
(125, 3) (7530, 3)
""" """
# Requires numpy 1.13 # Requires numpy 1.13
...@@ -314,7 +318,16 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -314,7 +318,16 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Translation in [x, y, z] Translation in [x, y, z]
""" """
self.vert[:] += trans
# Check that trans is array like
if isinstance(trans, (list, np.ndarray, tuple)):
# Check that trans has exactly 3 dimensions
if len(trans) == 3:
self.vert[:] += trans
else:
raise ValueError("Translation has incorrect dimensions. Expected 3 but found: " + str(len(trans)))
else:
raise TypeError("Translation is not array_like: " + trans)
def centre(self): def centre(self):
r""" r"""
...@@ -337,10 +350,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -337,10 +350,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples Examples
-------- --------
>>> amp = AmpObject('test.stl') >>> amp = AmpObject(filename)
>>> ang = [np.pi/2, -np.pi/4, np.pi/3] >>> ang = [np.pi/2, -np.pi/4, np.pi/3]
>>> amp.rotateAng(ang, ang='rad') >>> amp.rotateAng(ang, ang='rad')
""" """
# Check that ang is valid
if ang not in ('rad', 'deg'):
raise ValueError("Ang expected 'rad' or 'deg' but {} was found".format(ang))
if isinstance(rot, (tuple, list, np.ndarray)): if isinstance(rot, (tuple, list, np.ndarray)):
R = self.rotMatrix(rot, ang) R = self.rotMatrix(rot, ang)
self.rotate(R, norms) self.rotate(R, norms)
...@@ -359,6 +377,18 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -359,6 +377,18 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
norms: boolean, default True norms: boolean, default True
""" """
if isinstance(R, (list, tuple)):
# Make R a np array if its a list or tuple
R = np.array(R, np.float)
elif not isinstance(R, np.ndarray):
# If
raise TypeError("Expected R to be array-like but found: " + str(type(R)))
if len(R) != 3 or len(R[0]) != 3:
# Incorrect dimensions
if isinstance(R, np.ndarray):
raise ValueError("Expected 3x3 array, but found: {}".format(R.shape))
else:
raise ValueError("Expected 3x3 array, but found: 3x"+str(len(R)))
self.vert[:, :] = np.dot(self.vert, R.T) self.vert[:, :] = np.dot(self.vert, R.T)
if norms is True: if norms is True:
self.norm[:, :] = np.dot(self.norm, R.T) self.norm[:, :] = np.dot(self.norm, R.T)
...@@ -380,9 +410,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -380,9 +410,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
""" """
if R is not None: if R is not None:
self.rotate(R, True) if isinstance(R, (tuple, list, np.ndarray)):
self.rotate(R, True)
else:
raise TypeError("Expecting array-like rotation, but found: "+type(R))
if T is not None: if T is not None:
self.translate(T) if isinstance(T, (tuple, list, np.ndarray)):
self.translate(T)
else:
raise TypeError("Expecting array-like translation, but found: "+type(T))
@staticmethod @staticmethod
...@@ -396,7 +432,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -396,7 +432,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
rot: array_like rot: array_like
Rotation around [x, y, z] Rotation around [x, y, z]
ang: str, default 'rad' ang: str, default 'rad'
Specift if the Euler angles are in degrees or radians Specify if the Euler angles are in degrees or radians
Returns Returns
------- -------
...@@ -404,8 +440,20 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -404,8 +440,20 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
The calculated 3x3 rotation matrix The calculated 3x3 rotation matrix
""" """
# Check that rot is valid
if not isinstance(rot, (tuple, list, np.ndarray)):
raise TypeError("Expecting array-like rotation, but found: "+type(rot))
elif len(rot) != 3:
raise ValueError("Expecting 3 arguments but found: {}".format(len(rot)))
# Check that ang is valid
if ang not in ('rad', 'deg'):
raise ValueError("Ang expected 'rad' or 'deg' but {} was found".format(ang))
if ang == 'deg': if ang == 'deg':
rot = np.deg2rad(rot) rot = np.deg2rad(rot)
[angx, angy, angz] = rot [angx, angy, angz] = rot
Rx = np.array([[1, 0, 0], Rx = np.array([[1, 0, 0],
[0, np.cos(angx), -np.sin(angx)], [0, np.cos(angx), -np.sin(angx)],
...@@ -429,8 +477,14 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin): ...@@ -429,8 +477,14 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
The axis in which to flip the mesh The axis in which to flip the mesh
""" """
self.vert[:, axis] *= -1.0 if isinstance(axis, int):
# Switch face order to normals face same direction if 0 <= axis < 3: # Check axis is between 0-2
self.faces[:, [1, 2]] = self.faces[:, [2, 1]] self.vert[:, axis] *= -1.0
self.calcNorm() # Switch face order to normals face same direction
self.calcVNorm() self.faces[:, [1, 2]] = self.faces[:, [2, 1]]
self.calcNorm()
self.calcVNorm()
else:
raise ValueError("Expected axis to be within range 0-2 but found: {}".format(axis))
else:
raise TypeError("Expected axis to be int, but found: {}".format(type(axis)))
...@@ -6,9 +6,14 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk ...@@ -6,9 +6,14 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk
import numpy as np import numpy as np
import copy import copy
from scipy import spatial from scipy import spatial
from .core import AmpObject from AmpScan.core import AmpObject
import matplotlib.pyplot as plt import matplotlib.pyplot as plt
# For the doc examples
import os
basefh = os.getcwd()+"\\tests\\stl_file.stl"
targfh = os.getcwd()+"\\tests\\stl_file_2.stl"
class registration(object): class registration(object):
r""" r"""
Registration methods between two AmpObject meshes. This function morphs the baseline Registration methods between two AmpObject meshes. This function morphs the baseline
...@@ -36,9 +41,10 @@ class registration(object): ...@@ -36,9 +41,10 @@ class registration(object):
Examples Examples
-------- --------
>>> baseline = AmpScan.AmpObject(basefh) >>> from AmpScan.core import AmpObject
>>> target = AmpScan.AmpObject(targfh) >>> baseline = AmpObject(basefh)
>>> reg = AmpScan.registration(steps=10, neigh=10, smooth=1).reg >>> target = AmpObject(targfh)
>>> reg = registration(baseline, target, steps=10, neigh=10, smooth=1).reg
""" """
def __init__(self, baseline, target, method='point2plane', *args, **kwargs): def __init__(self, baseline, target, method='point2plane', *args, **kwargs):
...@@ -86,53 +92,52 @@ class registration(object): ...@@ -86,53 +92,52 @@ class registration(object):
[self.b.vert, self.b.faces, self.b.values])) [self.b.vert, self.b.faces, self.b.values]))
regData = copy.deepcopy(bData) regData = copy.deepcopy(bData)
self.reg = AmpObject(regData, stype='reg') 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: if scale is not None:
tmin = self.t.vert.min(axis=0)[2] tmin = self.t.vert.min(axis=0)[2]
rmin = self.reg.vert.min(axis=0)[2] rmin = self.reg.vert.min(axis=0)[2]
SF = ((tmin-scale)/(rmin-scale)) - 1 SF = ((tmin-scale)/(rmin-scale)) - 1
logic = self.reg.vert[:, 2] < scale logic = self.reg.vert[:, 2] < scale
d = (self.reg.vert[logic, 2] - scale) * SF d = (self.reg.vert[logic, 2] - scale) * SF
self.reg.vert[logic, 2] += d 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]] - normals = np.cross(self.t.vert[self.t.faces[:,1]] -
self.t.vert[self.t.faces[:,0]], self.t.vert[self.t.faces[:,0]],
self.t.vert[self.t.faces[:,2]] - self.t.vert[self.t.faces[:,2]] -
self.t.vert[self.t.faces[:,0]]) self.t.vert[self.t.faces[:,0]])
mag = (normals**2).sum(axis=1) mag = (normals**2).sum(axis=1)
if subset is None:
rVert = self.reg.vert
else:
rVert = self.reg.vert[subset]
for step in np.arange(steps, 0, -1, dtype=float): for step in np.arange(steps, 0, -1, dtype=float):
# Index of 10 centroids nearest to each baseline vertex # Index of 10 centroids nearest to each baseline vertex
ind = tTree.query(rVert, neigh)[1] ind = tTree.query(self.reg.vert, neigh)[1]
# D = np.zeros(self.reg.vert.shape)
# Define normals for faces of nearest faces # Define normals for faces of nearest faces
norms = normals[ind] norms = normals[ind]
# Get a point on each face # Get a point on each face
fPoints = self.t.vert[self.t.faces[ind, 0]] fPoints = self.t.vert[self.t.faces[ind, 0]]
# Calculate dot product between point on face and normals # Calculate dot product between point on face and normals
d = np.einsum('ijk, ijk->ij', norms, fPoints) d = np.einsum('ijk, ijk->ij', norms, fPoints)
t = (d - np.einsum('ijk, ik->ij', norms, rVert))/mag[ind] t = (d - np.einsum('ijk, ik->ij', norms, self.reg.vert))/mag[ind]
# Calculate the vector from old point to new point # Calculate the vector from old point to new point
G = rVert[:, None, :] + np.einsum('ijk, ij->ijk', norms, t) G = self.reg.vert[:, None, :] + np.einsum('ijk, ij->ijk', norms, t)
# Ensure new points lie inside points otherwise set to 99999 # Ensure new points lie inside points otherwise set to 99999
# Find smallest distance from old to new point # Find smallest distance from old to new point
if inside is False: if inside is False:
G = G - rVert[:, None, :] G = G - self.reg.vert[:, None, :]
GMag = np.sqrt(np.einsum('ijk, ijk->ij', G, G)) GMag = np.sqrt(np.einsum('ijk, ijk->ij', G, G))
GInd = GMag.argmin(axis=1) GInd = GMag.argmin(axis=1)
else: else:
G, GInd = self.__calcBarycentric(rVert, G, ind) G, GInd = self.__calcBarycentric(self.reg.vert, G, ind)
# Define vector from baseline point to intersect point # Define vector from baseline point to intersect point
D = G[np.arange(len(G)), GInd, :] D = G[np.arange(len(G)), GInd, :]
rVert += D/step # rVert += D/step
self.disp.vert += D/step
if smooth > 0 and step > 1: if smooth > 0 and step > 1:
# v = self.reg.vert[~subset] self.disp.lp_smooth(smooth, brim = fixBrim)
self.reg.lp_smooth(smooth, brim = fixBrim) self.reg.vert = self.b.vert + self.disp.vert
# self.reg.vert[~subset] = v
else: else:
self.reg.vert = self.b.vert + self.disp.vert
self.reg.calcNorm() self.reg.calcNorm()
self.reg.calcStruct() self.reg.calcStruct()
self.reg.values[:] = self.calcError(error) self.reg.values[:] = self.calcError(error)
......
...@@ -52,7 +52,7 @@ class smoothMixin(object): ...@@ -52,7 +52,7 @@ class smoothMixin(object):
def smoothValues(self, n=1): def smoothValues(self, n=1):
""" """
Function to apply a simple laplacian smooth to the values array. Function to apply a simple laplacian smooth to the values array.
Identical to the vertex smoothing expect it applies the smoothing Identical to the vertex smoothing except it applies the smoothing
to the values to the values
Parameters Parameters
......
...@@ -9,6 +9,7 @@ import os ...@@ -9,6 +9,7 @@ import os
import numpy as np import numpy as np
from .core import AmpObject from .core import AmpObject
from .registration import registration from .registration import registration
import os
class pca(object): class pca(object):
...@@ -18,20 +19,20 @@ class pca(object): ...@@ -18,20 +19,20 @@ class pca(object):
Examples Examples
-------- --------
>>> import os
>>> p = pca() >>> p = pca()
>>> p.importFolder('/path/') >>> p.importFolder(os.getcwd()+"\\tests\\pca_tests")
>>> p.baseline('dir/baselinefh.stl') >>> p.setBaseline(os.getcwd()+"\\tests\\stl_file_3.stl")
>>> p.register(save = '/regpath/') >>> p.register(save=os.getcwd()+"\\tests\\pca_tests\\")
>>> p.pca() >>> p.pca()
>>> sfs = [0, 0.1, -0.5 ... 0] >>> sfs = [1, 2]
>>> newS = p.newShape(sfs) >>> newS = p.newShape(sfs)
""" """
def __init__(self): def __init__(self):
self.shapes = [] self.shapes = []
def setBaseline(self, baseline): def setBaseline(self, baseline):
r""" r"""
Function to set the baseline mesh used for registration of the Function to set the baseline mesh used for registration of the
...@@ -44,8 +45,7 @@ class pca(object): ...@@ -44,8 +45,7 @@ class pca(object):
""" """
self.baseline = AmpObject(baseline, 'limb') self.baseline = AmpObject(baseline, 'limb')
def importFolder(self, path, unify=True): def importFolder(self, path, unify=True):
r""" r"""
Function to import multiple stl files from folder into the pca object Function to import multiple stl files from folder into the pca object
...@@ -60,10 +60,10 @@ class pca(object): ...@@ -60,10 +60,10 @@ class pca(object):
""" """
self.fnames = [f for f in os.listdir(path) if f.endswith('.stl')] self.fnames = [f for f in os.listdir(path) if f.endswith('.stl')]
self.shapes = [AmpObject(path + f, 'limb', unify=unify) for f in self.fnames] self.shapes = [AmpObject(os.path.join(path, f), 'limb', unify=unify) for f in self.fnames]
for s in self.shapes: