Commit 13398e31 authored by jp6g18's avatar jp6g18

Merge branch 'Jack' into 'master'

Merge in Jack's changes

Closes #45 and #43

See merge request !23
parents 164f6938 7097056c
Pipeline #883 passed with stage
in 43 seconds
sample_job:
script: python tests/sample_test.py
#doctests and unitests:
# 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
import math
from scipy import spatial
from scipy.optimize import minimize
from .core import AmpObject
from .ampVis import vtkRenWin
from AmpScan.core import AmpObject
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):
......@@ -41,9 +46,9 @@ class align(object):
Examples
--------
>>> static = AmpScan.AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh)
>>> al = AmpScan.align(moving, static).m
>>> static = AmpObject(staticfh)
>>> moving = AmpObject(movingfh)
>>> al = align(moving, static).m
"""
......@@ -176,9 +181,9 @@ class align(object):
Examples
--------
>>> static = AmpScan.AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='linPoint2Plane').m
>>> static = AmpObject(staticfh)
>>> moving = AmpObject(movingfh)
>>> al = align(moving, static, method='linPoint2Plane').m
"""
cn = np.c_[np.cross(mv, sn), sn]
......@@ -229,9 +234,9 @@ class align(object):
Examples
--------
>>> static = AmpScan.AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='linPoint2Point').m
>>> static = AmpObject(staticfh)
>>> moving = AmpObject(movingfh)
>>> al = align(moving, static, method='linPoint2Point').m
"""
mCent = mv - mv.mean(axis=0)
......@@ -271,9 +276,9 @@ class align(object):
Examples
--------
>>> static = AmpScan.AmpObject(staticfh)
>>> moving = AmpScan.AmpObject(movingfh)
>>> al = AmpScan.align(moving, static, method='optPoint2Point', opt='SLSQP').m
>>> static = AmpObject(staticfh)
>>> moving = AmpObject(movingfh)
>>> al = align(moving, static, method='optPoint2Point', opt='SLSQP').m
"""
X = np.zeros(6)
......
......@@ -6,11 +6,17 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk
"""
import numpy as np
import os
import struct
from .trim import trimMixin
from .smooth import smoothMixin
from .analyse import analyseMixin
from .ampVis import visMixin
from AmpScan.trim import trimMixin
from AmpScan.smooth import smoothMixin
from AmpScan.analyse import analyseMixin
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):
r"""
......@@ -36,8 +42,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples
-------
>>> fh = 'test.stl'
>>> amp = AmpScan.AmpObject(fh)
>>> amp = AmpObject(filename)
"""
......@@ -149,13 +154,12 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples
--------
>>> fh = 'test.stl'
>>> amp = AmpObject(fh, unify=False)
>>> amp = AmpObject(filename, unify=False)
>>> amp.vert.shape
(600, 3)
(44832, 3)
>>> amp.unifyVert()
>>> amp.vert.shape
(125, 3)
(7530, 3)
"""
# Requires numpy 1.13
......@@ -314,7 +318,16 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
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):
r"""
......@@ -337,10 +350,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
Examples
--------
>>> amp = AmpObject('test.stl')
>>> amp = AmpObject(filename)
>>> ang = [np.pi/2, -np.pi/4, np.pi/3]
>>> 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)):
R = self.rotMatrix(rot, ang)
self.rotate(R, norms)
......@@ -359,6 +377,18 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
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)
if norms is True:
self.norm[:, :] = np.dot(self.norm, R.T)
......@@ -380,9 +410,15 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
"""
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:
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
......@@ -396,7 +432,7 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
rot: array_like
Rotation around [x, y, z]
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
-------
......@@ -404,8 +440,20 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
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':
rot = np.deg2rad(rot)
[angx, angy, angz] = rot
Rx = np.array([[1, 0, 0],
[0, np.cos(angx), -np.sin(angx)],
......@@ -429,8 +477,14 @@ class AmpObject(trimMixin, smoothMixin, analyseMixin, visMixin):
The axis in which to flip the mesh
"""
self.vert[:, axis] *= -1.0
# Switch face order to normals face same direction
self.faces[:, [1, 2]] = self.faces[:, [2, 1]]
self.calcNorm()
self.calcVNorm()
if isinstance(axis, int):
if 0 <= axis < 3: # Check axis is between 0-2
self.vert[:, axis] *= -1.0
# Switch face order to normals face same direction
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
import numpy as np
import copy
from scipy import spatial
from .core import AmpObject
from AmpScan.core import AmpObject
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):
r"""
Registration methods between two AmpObject meshes. This function morphs the baseline
......@@ -36,9 +41,10 @@ class registration(object):
Examples
--------
>>> baseline = AmpScan.AmpObject(basefh)
>>> target = AmpScan.AmpObject(targfh)
>>> reg = AmpScan.registration(steps=10, neigh=10, smooth=1).reg
>>> 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):
......
......@@ -52,7 +52,7 @@ class smoothMixin(object):
def smoothValues(self, n=1):
"""
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
Parameters
......
......@@ -19,20 +19,20 @@ class pca(object):
Examples
--------
>>> import os
>>> p = pca()
>>> p.importFolder('/path/')
>>> p.baseline('dir/baselinefh.stl')
>>> p.register(save = '/regpath/')
>>> p.importFolder(os.getcwd()+"\\tests\\pca_tests")
>>> p.setBaseline(os.getcwd()+"\\tests\\stl_file_3.stl")
>>> p.register(save=os.getcwd()+"\\tests\\pca_tests\\")
>>> p.pca()
>>> sfs = [0, 0.1, -0.5 ... 0]
>>> sfs = [1, 2]
>>> newS = p.newShape(sfs)
"""
def __init__(self):
self.shapes = []
def setBaseline(self, baseline):
r"""
Function to set the baseline mesh used for registration of the
......@@ -45,8 +45,7 @@ class pca(object):
"""
self.baseline = AmpObject(baseline, 'limb')
def importFolder(self, path, unify=True):
r"""
Function to import multiple stl files from folder into the pca object
......@@ -64,7 +63,7 @@ class pca(object):
self.shapes = [AmpObject(os.path.join(path, f), 'limb', unify=unify) for f in self.fnames]
for s in self.shapes:
s.lp_smooth(3, brim=True)
def sliceFiles(self, height):
r"""
Function to run a planar trim on all the training data for the PCA
......@@ -78,7 +77,7 @@ class pca(object):
"""
for s in self.shapes:
s.planarTrim(height)
def register(self, scale=None, save=None, baseline=True):
r"""
Register all the AmpObject training data to the baseline AmpObject
......@@ -105,8 +104,7 @@ class pca(object):
self.X = np.array([r.vert.flatten() for r in self.registered]).T
if baseline is True:
self.X = np.c_[self.X, self.baseline.vert.flatten()]
def pca(self):
r"""
Function to run mean centered pca using a singular value decomposition
......@@ -118,8 +116,8 @@ class pca(object):
(self.pca_U, self.pca_S, self.pca_V) = np.linalg.svd(X_meanC, full_matrices=False)
self.pc_weights = np.dot(np.diag(self.pca_S), self.pca_V)
self.pc_stdevs = np.std(self.pc_weights, axis=1)
def newShape(self, sfs, scale = 'eigs'):
def newShape(self, sfs, scale='eigs'):
r"""
Function to calculate a new shape based upon the eigenvalues
or stdevs
......@@ -134,11 +132,15 @@ class pca(object):
to standard deviations about the mean
"""
try: len(sfs) == len(self.pc_stdevs)
except: ValueError('sfs must be of the same length as the number of '
'principal components')
if not isinstance(sfs, (list, tuple, np.ndarray)):
raise TypeError('sfs is invalid type (expected array-like, found: {}'.format(type(sfs)))
if len(sfs) != len(self.pc_stdevs):
raise ValueError('sfs must be of the same length as the number of '
'principal components (expected {} but found {})'.format(len(self.pc_stdevs), len(sfs)))
if scale == 'eigs':
sf = (self.pca_U * sfs).sum(axis=1)
elif scale == 'std':
sf = (self.pca_U * self.pc_stdevs * sfs).sum(axis=1)
else:
raise ValueError("Invalid scale (expected 'eigs' or 'std' but found{}".format(scale))
return self.pca_mean + sf
......@@ -5,6 +5,12 @@ Copyright: Joshua Steer 2018, Joshua.Steer@soton.ac.uk
"""
import numpy as np
from numbers import Number
import os
# Used by doc tests
filename = os.getcwd() + "\\tests\\stl_file.stl"
class trimMixin(object):
r"""
......@@ -26,30 +32,32 @@ class trimMixin(object):
Examples
--------
>>> amp = AmpObject(fh)
>>> from AmpScan import AmpObject
>>> amp = AmpObject(filename)
>>> amp.planarTrim(100, 2)
"""
# if isinstance(height, float):
if isinstance(height, Number) and isinstance(plane, int):
# planar values for each vert on face
fv = self.vert[self.faces, plane]
# Number points on each face are above cut plane
fvlogic = (fv > height).sum(axis=1)
# Faces with points both above and below cut plane
adjf = self.faces[np.logical_or(fvlogic == 2, fvlogic == 1)]
# Get adjacent vertices
adjv = np.unique(adjf)
# Get vert above height and set to height
abvInd = adjv[self.vert[adjv, plane] > height]
self.vert[abvInd, plane] = height
# Find all verts above plane
delv = self.vert[:, plane] > height
# Reorder verts to account for deleted one
vInd = np.cumsum(~delv) - 1
self.faces = self.faces[fvlogic != 3, :]
self.faces = vInd[self.faces]
self.vert = self.vert[~delv, :]
self.values = self.values[~delv]
self.calcStruct()
# else:
# raise TypeError("height arg must be a float")
\ No newline at end of file
fv = self.vert[self.faces, plane]
# Number points on each face are above cut plane
fvlogic = (fv > height).sum(axis=1)
# Faces with points both above and below cut plane
adjf = self.faces[np.logical_or(fvlogic == 2, fvlogic == 1)]
# Get adjacent vertices
adjv = np.unique(adjf)
# Get vert above height and set to height
abvInd = adjv[self.vert[adjv, plane] > height]
self.vert[abvInd, plane] = height
# Find all verts above plane
delv = self.vert[:, plane] > height
# Reorder verts to account for deleted one
vInd = np.cumsum(~delv) - 1
self.faces = self.faces[fvlogic != 3, :]
self.faces = vInd[self.faces]
self.vert = self.vert[~delv, :]
self.values = self.values[~delv]
self.calcStruct()
else:
raise TypeError("height arg must be a float")
......@@ -10,9 +10,9 @@ from PyQt5.QtGui import (QColor, QFontMetrics, QImage, QPainter, QIcon,
QOpenGLVersionProfile)
from PyQt5.QtWidgets import (QAction, QApplication, QGridLayout, QHBoxLayout,
QMainWindow, QMessageBox, QComboBox, QButtonGroup,
QOpenGLWidget, QFileDialog,QLabel,QPushButton,
QOpenGLWidget, QFileDialog, QLabel, QPushButton,
QSlider, QWidget, QTableWidget, QTableWidgetItem,
QAbstractButton)
QAbstractButton, QErrorMessage)
class AmpScanGUI(QMainWindow):
......@@ -107,16 +107,19 @@ class AmpScanGUI(QMainWindow):
Numpy style docstring.
"""
self.alCont = AlignControls(self.filesDrop, self)
self.alCont.show()
self.alCont.centre.clicked.connect(self.centreMesh)
self.alCont.icp.clicked.connect(self.runICP)
self.alCont.xrotButton.buttonClicked[QAbstractButton].connect(self.rotatex)
self.alCont.yrotButton.buttonClicked[QAbstractButton].connect(self.rotatey)
self.alCont.zrotButton.buttonClicked[QAbstractButton].connect(self.rotatez)
self.alCont.xtraButton.buttonClicked[QAbstractButton].connect(self.transx)
self.alCont.ytraButton.buttonClicked[QAbstractButton].connect(self.transy)
self.alCont.ztraButton.buttonClicked[QAbstractButton].connect(self.transz)
if self.objectsReady(1):
self.alCont = AlignControls(self.filesDrop, self)
self.alCont.show()
self.alCont.centre.clicked.connect(self.centreMesh)
self.alCont.icp.clicked.connect(self.runICP)
self.alCont.xrotButton.buttonClicked[QAbstractButton].connect(self.rotatex)
self.alCont.yrotButton.buttonClicked[QAbstractButton].connect(self.rotatey)
self.alCont.zrotButton.buttonClicked[QAbstractButton].connect(self.rotatez)
self.alCont.xtraButton.buttonClicked[QAbstractButton].connect(self.transx)
self.alCont.ytraButton.buttonClicked[QAbstractButton].connect(self.transy)
self.alCont.ztraButton.buttonClicked[QAbstractButton].connect(self.transz)
else:
show_message("Must be at least 1 object loaded to run align")
def rotatex(self, button):
moving = str(self.alCont.moving.currentText())
......@@ -178,53 +181,59 @@ class AmpScanGUI(QMainWindow):
self.renWin.Render()
def runICP(self):
static = str(self.alCont.static.currentText())
moving = str(self.alCont.moving.currentText())
al = align(self.files[moving], self.files[static],
maxiter=10, method='linPoint2Plane').m
al.tform = vtk.vtkTransform()
al.tform.PostMultiply()
al.addActor()
al.actor.SetUserTransform(al.tform)
alName = moving + '_al'
self.files[alName] = al
self.filesDrop.append(alName)
self.fileManager.addRow(alName, self.files[alName])
self.fileManager.setTable(static, [1,0,0], 0.5, 2)
self.fileManager.setTable(moving, [1,1,1], 1, 0)
self.fileManager.setTable(alName, [0,0,1], 0.5, 2)
if hasattr(self, 'alCont'):
self.alCont.getNames()
if hasattr(self, 'regCont'):
self.regCont.getNames()
if self.objectsReady(1):
static = str(self.alCont.static.currentText())
moving = str(self.alCont.moving.currentText())
al = align(self.files[moving], self.files[static],
maxiter=10, method='linPoint2Plane').m
al.tform = vtk.vtkTransform()
al.tform.PostMultiply()
al.addActor()
al.actor.SetUserTransform(al.tform)
alName = moving + '_al'
self.files[alName] = al
self.filesDrop.append(alName)
self.fileManager.addRow(alName, self.files[alName])
self.fileManager.setTable(static, [1,0,0], 0.5, 2)
self.fileManager.setTable(moving, [1,1,1], 1, 0)
self.fileManager.setTable(alName, [0,0,1], 0.5, 2)
if hasattr(self, 'alCont'):
self.alCont.getNames()
if hasattr(self, 'regCont'):
self.regCont.getNames()
else:
show_message("Must be at least 2 objects loaded to run ICP")
def runRegistration(self):
c1 = [31.0, 73.0, 125.0]
c3 = [170.0, 75.0, 65.0]
c2 = [212.0, 221.0, 225.0]
CMap1 = np.c_[[np.linspace(st, en) for (st, en) in zip(c1, c2)]]
CMap2 = np.c_[[np.linspace(st, en) for (st, en) in zip(c2, c3)]]
CMap = np.c_[CMap1[:, :-1], CMap2]
self.CMapN2P = np.transpose(CMap)/255.0
self.CMap02P = np.flip(np.transpose(CMap1)/255.0, axis=0)
baseline = str(self.regCont.baseline.currentText())
target = str(self.regCont.target.currentText())
self.fileManager.setTable(baseline, [1,0,0], 0.5, 0)
self.fileManager.setTable(target, [0,0,1], 0.5, 0)
reg = registration(self.files[baseline], self.files[target], steps = 5,
smooth=1).reg
reg.addActor(CMap = self.CMap02P)
regName = target + '_reg'
self.files[regName] = reg
self.filesDrop.append(regName)
self.fileManager.addRow(regName, self.files[regName])
if hasattr(self, 'alCont'):
self.alCont.getNames()
if hasattr(self, 'regCont'):
self.regCont.getNames()
print('Run the Registration code between %s and %s' % (baseline, target))
if self.objectsReady(2):
# Needs to be at least 2 files to run registration
c1 = [31.0, 73.0, 125.0]
c3 = [170.0, 75.0, 65.0]
c2 = [212.0, 221.0, 225.0]
CMap1 = np.c_[[np.linspace(st, en) for (st, en) in zip(c1, c2)]]
CMap2 = np.c_[[np.linspace(st, en) for (st, en) in zip(c2, c3)]]
CMap = np.c_[CMap1[:, :-1], CMap2]
self.CMapN2P = np.transpose(CMap)/255.0
self.CMap02P = np.flip(np.transpose(CMap1)/255.0, axis=0)
baseline = str(self.regCont.baseline.currentText())
target = str(self.regCont.target.currentText())
self.fileManager.setTable(baseline, [1,0,0], 0.5, 0)
self.fileManager.setTable(target, [0,0,1], 0.5, 0)
reg = registration(self.files[baseline], self.files[target], steps = 5,
smooth=1).reg
reg.addActor(CMap = self.CMap02P)
regName = target + '_reg'
self.files[regName] = reg
self.filesDrop.append(regName)
self.fileManager.addRow(regName, self.files[regName])
if hasattr(self, 'alCont'):
self.alCont.getNames()
if hasattr(self, 'regCont'):
self.regCont.getNames()
print('Run the Registration code between %s and %s' % (baseline, target))
else:
show_message("Must be at least 2 objects loaded to run registration")
def register(self):
"""
......@@ -256,13 +265,15 @@ class AmpScanGUI(QMainWindow):
"""
FEname = QFileDialog.getOpenFileName(self, 'Open file',
filter="FE results (*.npy)")
self.renWin.setnumViewports(1)
self.FE = AmpObject([FEname[0],], stype='FE')
self.AmpObj.lp_smooth()
self.AmpObj.addActor(CMap=self.AmpObj.CMap02P, bands=5)
self.AmpObj.actor.setScalarRange(smin=0.0, smax=50)
self.renWin.renderActors(self.FE.actor, shading=True)
self.renWin.setScalarBar(self.FE.actor)
if FEname[0] != "": # Check that there was a file selected
print(FEname)
self.renWin.setnumViewports(1)