Commit 672f17f6 authored by Josef Brandt's avatar Josef Brandt

From Linux..

parent f3c17ebf
......@@ -36,6 +36,25 @@ from .unittests.test_gepard import testGepard
from .helperfunctions import getAppFolder
def excepthook(excType, excValue, tracebackobj):
"""
Global function to catch unhandled exceptions.
@param excType exception type
@param excValue exception value
@param tracebackobj traceback object
:return:
"""
tbinfofile = StringIO()
traceback.print_tb(tracebackobj, None, tbinfofile)
tbinfofile.seek(0)
tbinfo = tbinfofile.read()
logging.critical("Fatal error in program excecution!")
logging.critical(tbinfo)
from .errors import showErrorMessageAsWidget
showErrorMessageAsWidget(tbinfo)
class GEPARDMainWindow(QtWidgets.QMainWindow):
def __init__(self, logger):
super(GEPARDMainWindow, self).__init__()
......@@ -387,25 +406,6 @@ if __name__ == '__main__':
"""
app.closeAllWindows()
def excepthook(excType, excValue, tracebackobj):
"""
Global function to catch unhandled exceptions.
@param excType exception type
@param excValue exception value
@param tracebackobj traceback object
:return:
"""
tbinfofile = StringIO()
traceback.print_tb(tracebackobj, None, tbinfofile)
tbinfofile.seek(0)
tbinfo = tbinfofile.read()
logging.critical("Fatal error in program excecution!")
logging.critical(tbinfo)
from .errors import showErrorMessageAsWidget
showErrorMessageAsWidget(tbinfo)
sys.excepthook = excepthook
logger = logging.getLogger(__name__)
......
......@@ -20,6 +20,8 @@ If not, see <https://www.gnu.org/licenses/>.
"""
import numpy as np
from typing import List, TYPE_CHECKING
# from .. import dataset
from gepard import dataset
if TYPE_CHECKING:
from .particleCharacterization import FTIRAperture
......@@ -28,6 +30,9 @@ if TYPE_CHECKING:
class Particle(object):
def __init__(self):
super(Particle, self).__init__()
# from ...gepard.dataset import DataSet
self.uid: int = dataset.DataSet.getUID()
self.index: int = np.nan
self.longSize: float = np.nan
self.shortSize: float = np.nan
......@@ -50,7 +55,7 @@ class Particle(object):
meas.setHQI(100)
def getParticleAssignment(self) -> str:
assignment = 'Will not be measured'
assignment = 'Excluded from Spectrum Scan'
if len(self.measurements) > 0:
assignment = self.getMeasAssignmentWithHighestHQI()
return assignment
......
......@@ -94,9 +94,7 @@ def getFTIRAperture(partImg: np.ndarray) -> FTIRAperture:
return aperture
def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None, ftir: bool = False):
if fullimage is None:
fullimage = loadFullimageFromDataset(dataset)
def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, scenePyramid=None, zimg=None, ftir: bool = False):
if zimg is None:
zimg = loadZValImageFromDataset(dataset)
......@@ -119,7 +117,12 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None,
newStats.longSize *= pixelscale
newStats.shortSize *= pixelscale
partImg = None
if scenePyramid is None and fullimage is not None:
partImg, extrema = getParticleImageFromFullimage(cnt, fullimage)
elif scenePyramid is not None and fullimage is None:
partImg, extrema = getParticleImageFromScenePyramid(cnt, scenePyramid)
assert partImg is not None, "error in getting particle image"
newStats.color = getParticleColor(partImg)
if ftir:
......@@ -168,7 +171,7 @@ def getParticleHeight(contour, fullZimg, dataset):
zimg = cv2.medianBlur(zimg, 5)
avg_ZValue = np.mean(zimg[zimg > 0])
if np.isnan(avg_ZValue): #i.e., only zeros in zimg
if np.isnan(avg_ZValue): # i.e., only zeros in zimg
avg_ZValue = 0
z0, z1 = dataset.zpositions.min(), dataset.zpositions.max()
height = avg_ZValue/255.*(z1-z0) + z0
......@@ -176,8 +179,8 @@ def getParticleHeight(contour, fullZimg, dataset):
def getContourStats(cnt):
##characterize particle
if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting...
# characterize particle
if cnt.shape[0] >= 5: # at least 5 points required for ellipse fitting...
ellipse = cv2.fitEllipse(cnt)
short, long = ellipse[1]
else:
......@@ -217,11 +220,30 @@ def getParticleImageFromFullimage(contour, fullimage):
return img, (xmin, xmax, ymin, ymax)
def getParticleImageFromScenePyramid(contour, scenePyramid):
contourCopy = deepcopy(contour)
xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
img = scenePyramid.getImagePart(ymin, ymax, xmin, xmax)
img = img.copy()
mask = np.zeros(img.shape[:2])
for i in range(len(contourCopy)):
contourCopy[i][0][0] -= xmin
contourCopy[i][0][1] -= ymin
cv2.drawContours(mask, [contourCopy], -1, (255, 255, 255), -1)
cv2.drawContours(mask, [contourCopy], -1, (255, 255, 255), 1)
img[mask == 0] = 0
img = np.array(img, dtype=np.uint8)
return img, (xmin, xmax, ymin, ymax)
def contoursToImg(contours, padding=0):
contourCopy = deepcopy(contours)
xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
padding = padding #pixel in each direction
rangex = int(np.round((xmax-xmin)+2*padding))
rangey = int(np.round((ymax-ymin)+2*padding))
if rangex == 0 or rangey == 0:
......@@ -239,35 +261,38 @@ def contoursToImg(contours, padding=0):
def imgToCnt(img, xmin, ymin, padding=0):
def getContour(img, contourMode):
def getLargestContour(contours):
areas = []
for contour in contours:
areas.append(cv2.contourArea(contour))
maxIndex = areas.index(max(areas))
print(f'{len(contours)} contours found, getting the largest one. Areas are: {areas}, '
f'taking contour at index {maxIndex}')
return contours[maxIndex]
if cv2.__version__ > '3.5':
contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, contourMode)
else:
temp, contours, hierarchy = cv2.findContours(img, cv2.RETR_EXTERNAL, contourMode)
if len(contours) == 0: #i.e., no contour found
if len(contours) == 0: # i.e., no contour found
raise InvalidParticleError
elif len(contours) == 1: #i.e., exactly one contour found
elif len(contours) == 1: # i.e., exactly one contour found
contour = contours[0]
else: #i.e., multiple contours found
else: # i.e., multiple contours found
contour = getLargestContour(contours)
return contour
def getLargestContour(contours):
areas = []
for contour in contours:
areas.append(cv2.contourArea(contour))
maxIndex = areas.index(max(areas))
print(f'{len(contours)} contours found, getting the largest one. Areas are: {areas}, taking contour at index {maxIndex}')
return contours[maxIndex]
img = closeHolesOfSubImage(img)
contour = getContour(img, contourMode=cv2.CHAIN_APPROX_NONE)
for i in range(len(contour)):
contour [i][0][0] += xmin-padding
contour [i][0][1] += ymin-padding
contour[i][0][0] += xmin-padding
contour[i][0][1] += ymin-padding
return contour
......
......@@ -22,6 +22,7 @@ If not, see <https://www.gnu.org/licenses/>.
import cv2
from ...errors import InvalidParticleError
class ShapeClassifier(object):
def __init__(self):
self.shapeClasses = [Spherule(), Fibre(), Irregular()]
......
......@@ -24,20 +24,41 @@ import os
from PyQt5 import QtWidgets
from typing import List, TYPE_CHECKING
from . import importSpectra
from .particleAndMeasurement import Particle, Measurement
specImportEnabled: bool = True
try:
from ..gepardevaluation.analysis import importSpectra
except ModuleNotFoundError:
specImportEnabled = False
if TYPE_CHECKING:
from ..dataset import DataSet
class ParticleContainer(object):
def __init__(self, datasetParent: 'DataSet'):
def __init__(self):
super(ParticleContainer, self).__init__()
self.datasetParent: 'DataSet' = datasetParent
self.datasetParent: 'DataSet' = None
self.particles: List[Particle] = []
self.measurements: List[Measurement] = []
self.inconsistentParticles: List[Particle] = []
def setDataSet(self, ds):
"""
dynamically sets a reference to the dataset containing this particle container
:param ds:
:return:
"""
self.datasetParent = ds
def unsetDataSet(self):
"""
is called before dataset is saved, preventing pickle to resolve this reference to dataset into
a copy of it (baking its data into the save file)
:return:
"""
self.datasetParent = None
def addEmptyMeasurement(self) -> int:
newMeas = Measurement()
self.measurements.append(newMeas)
......@@ -58,30 +79,27 @@ class ParticleContainer(object):
self.measurements[indexOfMeasurment].pixelcoord_x = x
self.measurements[indexOfMeasurment].pixelcoord_y = y
def getMeasurementScanindex(self, indexOfMeasurement):
def getMeasurementScanindex(self, indexOfMeasurement) -> int:
return self.measurements[indexOfMeasurement].getScanIndex()
def getSpectraFromDisk(self):
def getSpectraFromDisk(self) -> np.ndarray:
spectra = None
if specImportEnabled:
specPath = self.datasetParent.getSpectraFileName()
if os.path.exists(specPath):
spectra = np.load(specPath)
else:
fname = QtWidgets.QFileDialog.getOpenFileName(QtWidgets.QWidget(), 'Select Spectra File', self.datasetParent.path, 'text file (*.txt)')[0]
fname = QtWidgets.QFileDialog.getOpenFileName(QtWidgets.QWidget(), 'Select Spectra File',
self.datasetParent.path, 'text file (*.txt)')[0]
if fname:
#TODO: implement a more elegant way of testing through the individual imports...
try:
spectra, spectraNames = importSpectra.importWITecSpectra(fname)
except ImportError:
try:
spectra, spectraNames = importSpectra.importRenishawSpectra(fname)
except ImportError:
try:
spectra, spectraNames = importSpectra.importPerkinElmerSpectra(fname)
except ImportError:
pass
instruments = importSpectra.listInstruments()
instrument, okPressed = QtWidgets.QInputDialog.getItem(QtWidgets.QWidget(), "File format",
"Select instrument:", instruments, 0, False)
if okPressed and instrument:
spectra, spectraNames = importSpectra.chooseInstrument(instrument, fname)
except (ValueError, ImportError):
QtWidgets.QMessageBox.warning(QtWidgets.QWidget(), 'Error', 'Unknown format, no spectra loaded.')
if spectra is not None:
np.save(self.datasetParent.getSpectraFileName(), spectra)
......@@ -104,7 +122,8 @@ class ParticleContainer(object):
for index, particle in enumerate(self.particles):
particle.__dict__.update(particlestats[index].__dict__)
def testForInconsistentParticleAssignments(self): #i.e., particles that have multiple measurements with different assignments
def testForInconsistentParticleAssignments(self):
# Find particles that have multiple measurements with different assignments
self.inconsistentParticles = []
for particle in self.particles:
if not particle.measurementsHaveSameOrigAssignment():
......@@ -163,6 +182,18 @@ class ParticleContainer(object):
return particle
def getParticleOfUID(self, uid):
try:
for particle in self.particles:
if uid == particle.uid:
break
except:
print('failed getting particle')
print('requested Index:', uid)
print('len particles', len(self.particles))
assert particle.uid == uid, f'particle.index ({particle.uid}) does match requested index in particleList ({uid})'
return particle
def getParticleIndexContainingSpecIndex(self, index):
for particle in self.particles:
if index in particle.getMeasurementIndices():
......@@ -196,6 +227,10 @@ class ParticleContainer(object):
particle = self.getParticleOfIndex(partIndex)
return particle.getParticleAssignment()
def getParticleAssignmentByUID(self, uid):
particle = self.getParticleOfUID(uid)
return particle.getParticleAssignment()
def getMeasurementPixelCoords(self) -> list:
coords: list = []
for meas in self.measurements:
......@@ -209,7 +244,6 @@ class ParticleContainer(object):
coords.append([particle.aperture.centerX, particle.aperture.centerY])
return coords
def getNumberOfParticlesOfAssignment(self, assignment):
num = 0
for particle in self.particles:
......@@ -225,6 +259,10 @@ class ParticleContainer(object):
particle = self.getParticleOfIndex(particleIndex)
return particle.getMeasurementIndices()
def getSpectraIndicesOfParticleByUID(self, particleUID):
particle = self.getParticleOfUID(particleUID)
return particle.getMeasurementIndices()
def getListOfParticleAssignments(self):
particleAssignments = []
for particle in self.particles:
......@@ -280,6 +318,14 @@ class ParticleContainer(object):
particle = self.getParticleOfIndex(particleIndex)
return particle.shape
def getParticleIndexByUID(self, uid):
for idx, particle in enumerate(self.particles):
if uid == particle.uid:
break
assert uid == particle.uid
return idx
def getSizesOfParticleType(self, assignment):
particleSizes = []
for particle in self.particles:
......@@ -294,6 +340,13 @@ class ParticleContainer(object):
indices.append(particle.index)
return indices
def getUIDsOfParticleType(self, assignment):
ids = []
for particle in self.particles:
if particle.getParticleAssignment() == assignment:
ids.append(particle.uid)
return ids
def getSizeOfParticleByIndex(self, index):
particle = self.getParticleOfIndex(index)
return particle.getParticleSize()
......@@ -325,13 +378,19 @@ class ParticleContainer(object):
def changeParticleShape(self, index, newShape):
particle = self.getParticleOfIndex(index)
if 'fibre' == newShape:
particle.longSize, particle.shortSize = pc.getFibreDimension(particle.contour)
particle.longSize *= self.datasetParent.getPixelScale()
particle.shortSize *= self.datasetParent.getPixelScale()
particle.shape = newShape
particle.wasManuallyEdited = True
def addMergedParticle(self, particleIndices, newContour, newStats, newAssignment=None):
def addMergedParticle(self, particleIndices, newContour, newStats, newAssignment=None) -> Particle:
newParticle = Particle()
newParticle.contour = newContour
#copy Measurements
# copy Measurements
for index in particleIndices:
particle = self.getParticleOfIndex(index)
for meas in particle.getMeasurements():
......@@ -343,7 +402,12 @@ class ParticleContainer(object):
newParticle.__dict__.update(newStats.__dict__)
newParticle.wasManuallyEdited = True
self.particles.append(newParticle)
print('added new particle')
return newParticle
def removeParticles(self, indices):
assert isinstance(indices, list)
for idx in indices:
self.removeParticle(idx)
def removeParticle(self, index):
particle = self.getParticleOfIndex(index) #just for asserting to have the correct particle!
......
......@@ -25,7 +25,6 @@ from PyQt5 import QtWidgets, QtCore
from .particlePainter import ParticlePainter
from . import particleCharacterization as pc
from ..errors import NotConnectedContoursError
from ..helperfunctions import hasFTIRControl
class ParticleContextMenu(QtWidgets.QMenu):
......@@ -38,18 +37,29 @@ class ParticleContextMenu(QtWidgets.QMenu):
def __init__(self, viewparent):
super(ParticleContextMenu, self).__init__()
self.viewparent = viewparent
self.selectedParticleIndices = self.viewparent.selectedParticleIndices
self.particleContainer = self.viewparent.dataset.particleContainer
self.shapeMenu = QtWidgets.QMenu("Set Particle Shape To")
self.colorMenu = QtWidgets.QMenu("Set Particle Color To")
self.reassignMenu = QtWidgets.QMenu("Reassign particle(s) into")
self.paintMenu = QtWidgets.QMenu("Paint Mode, merge into")
self.combineMenu = QtWidgets.QMenu("Combine Particles into")
def executeAtScreenPos(self, screenPos):
self.deleteAct = self.addAction("Delete particle(s)")
self.shapeActs = []
self.colorActs = []
self.reassignActs = []
self.paintActs = []
self.combineActs = []
self.viewparent = viewparent
self.selectedParticleIndices = []
self.particleContainer = viewparent.dataset.particleContainer
self.createActsAndMenus()
def executeAtScreenPos(self, screenPos):
action = self.exec_(screenPos)
if action:
actionText = self.validifyText(action.text())
actionText = self._valifyText(action.text())
if action in self.combineActs:
self.combineParticlesSignal.emit(self.selectedParticleIndices, actionText)
elif action in self.reassignActs:
......@@ -64,53 +74,40 @@ class ParticleContextMenu(QtWidgets.QMenu):
self.deleteParticlesSignal.emit(self.selectedParticleIndices)
def createActsAndMenus(self):
self.combineActs = []
self.combineMenu = QtWidgets.QMenu("Combine Particles into")
self.paintActs = []
self.paintMenu = QtWidgets.QMenu("Paint Mode, merge into")
selctedAssignments = []
selected_assignments = []
self.selectedParticleIndices = self.viewparent.selectedParticleIndices
for particleIndex in self.selectedParticleIndices:
try:
assignment = self.particleContainer.getParticleAssignmentByIndex(particleIndex)
except:
return
selctedAssignments.append(assignment)
selected_assignments.append(assignment)
for assignment in np.unique(selctedAssignments):
for assignment in np.unique(selected_assignments):
self.combineActs.append(self.combineMenu.addAction(assignment))
self.paintActs.append(self.paintMenu.addAction(assignment))
self.combineActs.append(self.combineMenu.addAction("other"))
self.paintActs.append(self.paintMenu.addAction("other"))
self.reassignActs = []
self.reassignMenu = QtWidgets.QMenu("Reassign particle(s) into")
for polymType in self.particleContainer.getUniquePolymers():
self.reassignActs.append(self.reassignMenu.addAction(polymType))
self.reassignActs.append(self.reassignMenu.addAction("other"))
numParticles = len(self.selectedParticleIndices)
if numParticles == 0:
num_particles = len(self.selectedParticleIndices)
if num_particles == 0:
self.reassignMenu.setDisabled(True)
self.combineMenu.setDisabled(True)
elif numParticles == 1:
elif num_particles == 1:
self.combineMenu.setDisabled(True)
self.colorMenu = QtWidgets.QMenu("Set Particle Color To")
self.colorActs = []
for color in ['white', 'black', 'blue', 'brown', 'green', 'grey', 'non-determinable', 'red', 'transparent', 'yellow', 'violet']:
for color in ['white', 'black', 'blue', 'brown', 'green', 'grey', 'non-determinable', 'red', 'transparent',
'yellow', 'violet']:
self.colorActs.append(self.colorMenu.addAction(color))
self.shapeMenu = QtWidgets.QMenu("Set Particle Shape To")
self.shapeActs = []
for shape in ['fibre', 'spherule', 'irregular', 'flake']:
self.shapeActs.append(self.shapeMenu.addAction(shape))
infoAct = self.addAction(f'selected {numParticles} particles')
infoAct = self.addAction(f'selected {num_particles} particles')
infoAct.setDisabled(True)
self.addMenu(self.combineMenu)
self.addMenu(self.reassignMenu)
......@@ -119,14 +116,13 @@ class ParticleContextMenu(QtWidgets.QMenu):
self.addMenu(self.colorMenu)
self.addMenu(self.shapeMenu)
self.addSeparator()
self.deleteAct = self.addAction("Delete particle(s)")
def validifyText(self, assignment):
def _valifyText(self, assignment):
if assignment == "other":
assignment = self.getNewEntry()
assignment = self._getNewEntry()
return assignment
def getNewEntry(self):
def _getNewEntry(self):
text, okClicked = QtWidgets.QInputDialog.getText(self.viewparent, "Custom assignment", "Enter new assignment")
if okClicked and text != '':
return text
......@@ -135,14 +131,17 @@ class ParticleContextMenu(QtWidgets.QMenu):
class ParticleEditor(QtCore.QObject):
particleAssignmentChanged = QtCore.pyqtSignal()
def __init__(self, viewparent, particleContainer):
def __init__(self, viewparent, dataset):
super(ParticleEditor, self).__init__()
self.particleContainer = particleContainer
self.particleContainer = dataset.particleContainer
self.dataset = dataset
self.viewparent = viewparent # the assigned analysis widget
self.backupFreq = 3 # save a backup every n actions
self.neverBackedUp = True
self.actionCounter = 0
self.scenePyramid = viewparent.getScenePyramid()
self.storedIndices = []
self.storedAssignmend = None
......@@ -156,8 +155,8 @@ class ParticleEditor(QtCore.QObject):
def createSafetyBackup(self):
self.actionCounter += 1
if self.actionCounter == self.backupFreq-1 or self.neverBackedUp:
backupname = self.viewparent.dataset.saveBackup()
if self.actionCounter == self.backupFreq - 1 or self.neverBackedUp:
backupname = self.dataset.saveBackup()
print('backing up as', backupname)
self.neverBackedUp = False
self.actionCounter = 0
......@@ -233,12 +232,10 @@ class ParticleEditor(QtCore.QObject):
self.particlePainter = None
def mergeParticlesInParticleContainerAndSampleView(self, indices, newContour, assignment):
isFTIR: bool = hasFTIRControl(self.viewparent)
stats = pc.getParticleStatsWithPixelScale(newContour, self.viewparent.dataset,
fullimage=self.viewparent.imgdata, ftir=isFTIR)
stats = pc.getParticleStatsWithPixelScale(newContour, self.dataset, scenePyramid=self.scenePyramid)
self.viewparent.addParticleContourToIndex(newContour, len(self.viewparent.contourItems)-1)
self.particleContainer.addMergedParticle(indices, newContour, stats, newAssignment=assignment)
new_particle = self.particleContainer.addMergedParticle(indices, newContour, stats, newAssignment=assignment)
self.viewparent.addParticleContourToIndex(newContour, len(self.viewparent.contourItems) - 1, particle_uid=new_particle.uid)
for ind in sorted(indices, reverse=True):
self.viewparent.removeParticleContour(ind)
......@@ -246,7 +243,7 @@ class ParticleEditor(QtCore.QObject):
self.viewparent.resetContourIndices()
self.particleContainer.resetParticleIndices()
self.viewparent.updateParticleInfoBox(self.particleContainer.getNumberOfParticles()-1)
self.viewparent.updateParticleInfoBox(self.particleContainer.getNumberOfParticles() - 1)
self.particleAssignmentChanged.emit()
@QtCore.pyqtSlot(list, str)
......@@ -265,20 +262,20 @@ class ParticleEditor(QtCore.QObject):