Commit 893884dd authored by Josef Brandt's avatar Josef Brandt

Merge branch 'ThermoFTIR' into MergingIntoTiling2Develop

parents 2e71208e b2e6bcbc
...@@ -24,3 +24,7 @@ ramancom/renishawtesting.py ...@@ -24,3 +24,7 @@ ramancom/renishawtesting.py
*.lib *.lib
*.obj *.obj
*.pyc
*.jdx
...@@ -204,10 +204,6 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): ...@@ -204,10 +204,6 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
if self.view.simulatedRaman: if self.view.simulatedRaman:
self.configRamanCtrlAct.setDisabled(True) self.configRamanCtrlAct.setDisabled(True)
self.recalculateCoordAct = QtWidgets.QAction("&Recalculate Coordinate System")
self.recalculateCoordAct.setDisabled(True)
self.recalculateCoordAct.triggered.connect(self.view.recalculateCoordinateSystem)
self.noOverlayAct = QtWidgets.QAction("&No Overlay", self) self.noOverlayAct = QtWidgets.QAction("&No Overlay", self)
self.noOverlayAct.setShortcut("1") self.noOverlayAct.setShortcut("1")
self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self) self.selOverlayAct = QtWidgets.QAction("&Selected Overlay", self)
...@@ -296,7 +292,6 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): ...@@ -296,7 +292,6 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.toolsMenu = QtWidgets.QMenu("&Tools") self.toolsMenu = QtWidgets.QMenu("&Tools")
self.toolsMenu.addAction(self.snapshotAct) self.toolsMenu.addAction(self.snapshotAct)
self.toolsMenu.addAction(self.configRamanCtrlAct) self.toolsMenu.addAction(self.configRamanCtrlAct)
self.toolsMenu.addAction(self.recalculateCoordAct)
self.dispMenu = QtWidgets.QMenu("&Display", self) self.dispMenu = QtWidgets.QMenu("&Display", self)
self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu) self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu)
...@@ -389,7 +384,7 @@ if __name__ == '__main__': ...@@ -389,7 +384,7 @@ if __name__ == '__main__':
logging.handlers.RotatingFileHandler( logging.handlers.RotatingFileHandler(
logname, maxBytes=5*(1 << 20), backupCount=10) logname, maxBytes=5*(1 << 20), backupCount=10)
) )
logger.setLevel(logging.DEBUG)
setDefaultLoggingConfig(logger) setDefaultLoggingConfig(logger)
logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime())) logger.info("starting GEPARD at: " + strftime("%d %b %Y %H:%M:%S", localtime()))
......
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import numpy as np
from scipy import optimize
import cv2
from ..cythonModules.getMaxRect import findMaxRect
####################################################
'''Code taken from https://github.com/pogam/ExtractRect/blob/master/extractRect.py and modified for our use'''
####################################################
def residual(angle, img):
img_rot, rotationMatrix = rotateImageAroundAngle(img, angle)
point1, point2, width, height, max_area = findMaxRect(img_rot)
return 1/max_area
def getFinalRectangle(angle, img, shiftScaleParam):
ny = img.shape[1]
img_rot, rotationMatrix = rotateImageAroundAngle(img, angle)
point1, point2, width, height, max_area = findMaxRect(img_rot)
width *= shiftScaleParam.scale_factor
height *= shiftScaleParam.scale_factor
#invert rectangle
M_invert = cv2.invertAffineTransform(rotationMatrix)
rect_coord = [point1, [point1[0],point2[1]] ,
point2, [point2[0],point1[1]] ]
rect_coord_ori = []
for coord in rect_coord:
rect_coord_ori.append(np.dot(M_invert,[coord[0],(ny-1)-coord[1],1]))
#transform to numpy coord of input image
coord_out = []
for coord in rect_coord_ori:
coord_out.append([shiftScaleParam.scale_factor*round( coord[0],0)-shiftScaleParam.shift_x,\
shiftScaleParam.scale_factor*round((ny-1)-coord[1],0)-shiftScaleParam.shift_y])
#transpose back the original coords:
coord_out = transposeRectCoords(coord_out)
return coord_out, round(width), round(height)
def transposeRectCoords(rectCoords):
#mirror x and y:
newCoords = []
for i in range(len(rectCoords)):
newCoords.append([rectCoords[i][1], rectCoords[i][0]])
return newCoords
def addBorderToMakeImageSquare(img):
nx, ny = img.shape
if nx != ny:
n = max([nx,ny])
img_square = np.ones([n,n])
xshift = (n-nx)/2
yshift = (n-ny)/2
if yshift == 0:
img_square[int(xshift):int(xshift+nx),:] = img
else:
img_square[:,int(yshift):int(yshift+ny)] = img
else:
xshift = 0
yshift = 0
img_square = img
return img_square, xshift, yshift
def limitImageSize(img, limitSize):
if img.shape[0] > limitSize:
img_small = cv2.resize(img,(limitSize, limitSize),interpolation=0)
scale_factor = 1.*img.shape[0]/img_small.shape[0]
else:
img_small = img
scale_factor = 1
return img_small, scale_factor
def makeImageOddSized(img):
# set the input data with an odd number of point in each dimension to make rotation easier
nx,ny = img.shape
nx_extra = -nx
ny_extra = -ny
if nx%2==0:
nx+=1
nx_extra = 1
if ny%2==0:
ny+=1
ny_extra = 1
img_odd = np.ones([img.shape[0]+max([0,nx_extra]),img.shape[1]+max([0,ny_extra])], dtype=np.uint8)
img_odd[:-nx_extra, :-ny_extra] = img
return img_odd
def rotateImageAroundAngle(img, angle):
nx,ny = img.shape
rotationMatrix = cv2.getRotationMatrix2D(((nx-1)/2,(ny-1)/2),angle,1)
img_rot = np.array(cv2.warpAffine(img, rotationMatrix, img.shape, flags=cv2.INTER_NEAREST, borderValue=1))
img_rot = img_rot.astype(np.uint8)
return img_rot, rotationMatrix
def findRotatedMaximalRectangle(img, nbre_angle=4, limit_image_size=300):
img_square, shift_x, shift_y = addBorderToMakeImageSquare(img)
img_small, scale_factor = limitImageSize(img_square, limit_image_size)
img_odd = makeImageOddSized(img_small)
nx,ny = nx_odd, ny_odd = img_odd.shape
shiftScaleParam = ShiftAndScaleParam(shift_x, shift_y, scale_factor)
# angle_range = ([(90.,180.),])
angle_range = ([(0., 90.), ])
coeff1 = optimize.brute(residual, angle_range, args=(img_odd,), Ns=nbre_angle, finish=None)
popt = optimize.fmin(residual, coeff1, args=(img_odd,), xtol=5, ftol=1.e-5, disp=False)
opt_angle = popt[0]
rectangleCoords, width, height = getFinalRectangle(opt_angle, img_odd, shiftScaleParam)
return rectangleCoords, opt_angle, width, height
class ShiftAndScaleParam(object):
def __init__(self, shift_x=0, shift_y=0, scale_factor=1):
self.shift_x = shift_x
self.shift_y = shift_y
self.scale_factor = scale_factor
\ No newline at end of file
...@@ -29,6 +29,7 @@ class Particle(object): ...@@ -29,6 +29,7 @@ class Particle(object):
self.height = None self.height = None
self.area = None self.area = None
self.contour = None self.contour = None
self.aperture = None
self.measurements = [] self.measurements = []
self.color = None self.color = None
self.shape = None self.shape = None
...@@ -127,7 +128,8 @@ class Measurement(object): ...@@ -127,7 +128,8 @@ class Measurement(object):
self.assignment_orig = 'Not Evaluated' self.assignment_orig = 'Not Evaluated'
self.assignment_afterHQI = None self.assignment_afterHQI = None
self.hqi = 0 self.hqi = 0
self.measurementResults: dict = {}
self.assignedParticle = None self.assignedParticle = None
def setAssignment(self, assignment): def setAssignment(self, assignment):
......
...@@ -26,17 +26,34 @@ from copy import deepcopy ...@@ -26,17 +26,34 @@ from copy import deepcopy
from .particleClassification.colorClassification import ColorClassifier from .particleClassification.colorClassification import ColorClassifier
from .particleClassification.shapeClassification import ShapeClassifier from .particleClassification.shapeClassification import ShapeClassifier
from .ftirAperture import findRotatedMaximalRectangle
from ..segmentation import closeHolesOfSubImage from ..segmentation import closeHolesOfSubImage
from ..errors import InvalidParticleError from ..errors import InvalidParticleError
from ..helperfunctions import cv2imread_fix from ..helperfunctions import cv2imread_fix
class FTIRAperture(object):
"""
Configuration for an FTIR aperture. CenterCoords, width and height in Pixels
"""
centerX: float = None
centerY: float = None
centerZ: float = None
width: float = None
height: float = None
angle: float = None
rectCoords: list = None
class ParticleStats(object): class ParticleStats(object):
longSize = None longSize: float = None
shortSize = None shortSize: float = None
height = None height: float = None
area = None area: float = None
shape = None shape: str = None
color = None color: str = None
aperture: FTIRAperture = None
def particleIsValid(particle): def particleIsValid(particle):
if particle.longSize == 0 or particle.shortSize == 0: if particle.longSize == 0 or particle.shortSize == 0:
...@@ -45,7 +62,8 @@ def particleIsValid(particle): ...@@ -45,7 +62,8 @@ def particleIsValid(particle):
if cv2.contourArea(particle.contour) == 0: if cv2.contourArea(particle.contour) == 0:
return False return False
return True return True
def updateStatsOfParticlesIfNotManuallyEdited(particleContainer): def updateStatsOfParticlesIfNotManuallyEdited(particleContainer):
dataset = particleContainer.datasetParent dataset = particleContainer.datasetParent
fullimage = loadFullimageFromDataset(dataset) fullimage = loadFullimageFromDataset(dataset)
...@@ -59,6 +77,23 @@ def updateStatsOfParticlesIfNotManuallyEdited(particleContainer): ...@@ -59,6 +77,23 @@ def updateStatsOfParticlesIfNotManuallyEdited(particleContainer):
newStats = getParticleStatsWithPixelScale(particle.contour, dataset, fullimage, zimg) newStats = getParticleStatsWithPixelScale(particle.contour, dataset, fullimage, zimg)
particle.__dict__.update(newStats.__dict__) particle.__dict__.update(newStats.__dict__)
def getFTIRAperture(partImg: np.ndarray) -> FTIRAperture:
rectPoints, angle, width, heigth = findRotatedMaximalRectangle(partImg, nbre_angle=4, limit_image_size=300)
xvalues: list = [i[0] for i in rectPoints]
yvalues: list = [i[1] for i in rectPoints]
aperture = FTIRAperture()
aperture.height = heigth
aperture.width = width
aperture.angle = angle
aperture.centerX = int(round(min(xvalues) + (max(xvalues) - min(xvalues)) / 2))
aperture.centerY = int(round(min(yvalues) + (max(yvalues) - min(yvalues)) / 2))
aperture.rectCoords = rectPoints
return aperture
def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None): def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None):
if fullimage is None: if fullimage is None:
fullimage = loadFullimageFromDataset(dataset) fullimage = loadFullimageFromDataset(dataset)
...@@ -84,17 +119,31 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None): ...@@ -84,17 +119,31 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None):
newStats.longSize *= pixelscale newStats.longSize *= pixelscale
newStats.shortSize *= pixelscale newStats.shortSize *= pixelscale
partImg = getParticleImageFromFullimage(cnt, fullimage) partImg, extrema = getParticleImageFromFullimage(cnt, fullimage)
newStats.color = getParticleColor(partImg) newStats.color = getParticleColor(partImg)
padding: int = int(2)
imgforAperture = np.zeros((partImg.shape[0]+2*padding, partImg.shape[1]+2*padding))
imgforAperture[padding:partImg.shape[0]+padding, padding:partImg.shape[1]+padding] = np.mean(partImg, axis=2)
imgforAperture[imgforAperture > 0] = 1 # convert to binary image
imgforAperture = np.uint8(1 - imgforAperture) # background has to be 1, particle has to be 0
aperture: FTIRAperture = getFTIRAperture(imgforAperture)
aperture.centerX = aperture.centerX + extrema[0] - padding
aperture.centerY = aperture.centerY + extrema[2] - padding
aperture.centerZ = newStats.height
newStats.aperture = aperture
return newStats return newStats
def getFibreDimension(contour): def getFibreDimension(contour):
longSize = cv2.arcLength(contour, True)/2 longSize = cv2.arcLength(contour, True)/2
img = contoursToImg([contour])[0] img = contoursToImg([contour])[0]
dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) dist = cv2.distanceTransform(img, cv2.DIST_L2, 3)
maxThickness = np.max(dist)*2 maxThickness = np.max(dist)*2
return longSize, maxThickness return longSize, maxThickness
def getParticleColor(imgRGB, colorClassifier=None): def getParticleColor(imgRGB, colorClassifier=None):
img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL) img = cv2.cvtColor(imgRGB, cv2.COLOR_RGB2HSV_FULL)
meanHSV = cv2.mean(img) meanHSV = cv2.mean(img)
...@@ -103,14 +152,16 @@ def getParticleColor(imgRGB, colorClassifier=None): ...@@ -103,14 +152,16 @@ def getParticleColor(imgRGB, colorClassifier=None):
color = colorClassifier.classifyColor(meanHSV) color = colorClassifier.classifyColor(meanHSV)
return color return color
def getParticleShape(contour, particleHeight, shapeClassifier=None): def getParticleShape(contour, particleHeight, shapeClassifier=None):
if shapeClassifier is None: if shapeClassifier is None:
shapeClassifier = ShapeClassifier() shapeClassifier = ShapeClassifier()
shape = shapeClassifier.classifyShape(contour, particleHeight) shape = shapeClassifier.classifyShape(contour, particleHeight)
return shape return shape
def getParticleHeight(contour, fullZimg, dataset): def getParticleHeight(contour, fullZimg, dataset):
zimg = getParticleImageFromFullimage(contour, fullZimg) zimg, _extrema = getParticleImageFromFullimage(contour, fullZimg)
if zimg.shape[0] == 0 or zimg.shape[1] == 0: if zimg.shape[0] == 0 or zimg.shape[1] == 0:
raise InvalidParticleError raise InvalidParticleError
...@@ -122,6 +173,7 @@ def getParticleHeight(contour, fullZimg, dataset): ...@@ -122,6 +173,7 @@ def getParticleHeight(contour, fullZimg, dataset):
height = avg_ZValue/255.*(z1-z0) + z0 height = avg_ZValue/255.*(z1-z0) + z0
return height return height
def getContourStats(cnt): def getContourStats(cnt):
##characterize particle ##characterize particle
if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting... if cnt.shape[0] >= 5: ##at least 5 points required for ellipse fitting...
...@@ -138,10 +190,12 @@ def getContourStats(cnt): ...@@ -138,10 +190,12 @@ def getContourStats(cnt):
return long, short, area return long, short, area
def mergeContours(contours): def mergeContours(contours):
img, xmin, ymin, padding = contoursToImg(contours) img, xmin, ymin, padding = contoursToImg(contours)
return imgToCnt(img, xmin, ymin, padding) return imgToCnt(img, xmin, ymin, padding)
def getParticleImageFromFullimage(contour, fullimage): def getParticleImageFromFullimage(contour, fullimage):
contourCopy = deepcopy(contour) contourCopy = deepcopy(contour)
xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
...@@ -159,8 +213,9 @@ def getParticleImageFromFullimage(contour, fullimage): ...@@ -159,8 +213,9 @@ def getParticleImageFromFullimage(contour, fullimage):
img[mask == 0] = 0 img[mask == 0] = 0
img = np.array(img, dtype = np.uint8) img = np.array(img, dtype = np.uint8)
return img return img, (xmin, xmax, ymin, ymax)
def contoursToImg(contours, padding=0): def contoursToImg(contours, padding=0):
contourCopy = deepcopy(contours) contourCopy = deepcopy(contours)
xmin, xmax, ymin, ymax = getContourExtrema(contourCopy) xmin, xmax, ymin, ymax = getContourExtrema(contourCopy)
...@@ -181,6 +236,7 @@ def contoursToImg(contours, padding=0): ...@@ -181,6 +236,7 @@ def contoursToImg(contours, padding=0):
img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3)))) img = np.uint8(cv2.morphologyEx(img, cv2.MORPH_CLOSE, np.ones((3, 3))))
return img, xmin, ymin, padding return img, xmin, ymin, padding
def imgToCnt(img, xmin, ymin, padding=0): def imgToCnt(img, xmin, ymin, padding=0):
def getContour(img, contourMode): def getContour(img, contourMode):
if cv2.__version__ > '3.5': if cv2.__version__ > '3.5':
...@@ -214,6 +270,7 @@ def imgToCnt(img, xmin, ymin, padding=0): ...@@ -214,6 +270,7 @@ def imgToCnt(img, xmin, ymin, padding=0):
return contour return contour
def getContourExtrema(contours): def getContourExtrema(contours):
try: try:
cnt = np.vstack(tuple(contours)) cnt = np.vstack(tuple(contours))
...@@ -228,6 +285,7 @@ def getContourExtrema(contours): ...@@ -228,6 +285,7 @@ def getContourExtrema(contours):
return xmin, xmax, ymin, ymax return xmin, xmax, ymin, ymax
def getParticleCenterPoint(contour): def getParticleCenterPoint(contour):
img, xmin, ymin, padding = contoursToImg(contour) img, xmin, ymin, padding = contoursToImg(contour)
dist = cv2.distanceTransform(img, cv2.DIST_L2, 3) dist = cv2.distanceTransform(img, cv2.DIST_L2, 3)
...@@ -238,8 +296,10 @@ def getParticleCenterPoint(contour): ...@@ -238,8 +296,10 @@ def getParticleCenterPoint(contour):
y += ymin y += ymin
return x, y return x, y
def loadFullimageFromDataset(dataset): def loadFullimageFromDataset(dataset):
return cv2imread_fix(dataset.getImageName()) return cv2imread_fix(dataset.getImageName())
def loadZValImageFromDataset(dataset): def loadZValImageFromDataset(dataset):
return cv2imread_fix(dataset.getZvalImageName(), cv2.IMREAD_GRAYSCALE) return cv2imread_fix(dataset.getZvalImageName(), cv2.IMREAD_GRAYSCALE)
\ No newline at end of file
...@@ -34,15 +34,15 @@ class ParticleContainer(object): ...@@ -34,15 +34,15 @@ class ParticleContainer(object):
self.measurements = [] self.measurements = []
self.inconsistentParticles = [] self.inconsistentParticles = []
def addEmptyMeasurement(self): def addEmptyMeasurement(self) -> int:
newMeas = Measurement() newMeas = Measurement()
self.measurements.append(newMeas) self.measurements.append(newMeas)
return self.measurements.index(newMeas) return self.measurements.index(newMeas)
def clearParticles(self): def clearParticles(self) -> None:
self.particles = [] self.particles = []
def clearMeasurements(self): def clearMeasurements(self) -> None:
self.measurements = [] self.measurements = []
for particle in self.particles: for particle in self.particles:
particle.measurements = [] particle.measurements = []
...@@ -132,6 +132,13 @@ class ParticleContainer(object): ...@@ -132,6 +132,13 @@ class ParticleContainer(object):
for meas in self.measurements: for meas in self.measurements:
scanIndex = meas.getScanIndex() scanIndex = meas.getScanIndex()
meas.setAssignment(assignmentList[scanIndex]) meas.setAssignment(assignmentList[scanIndex])
def applyDatabaseResultsToParticleMeasurements(self, resultDictList: list) -> None:
"""
The resultDictList has a dict for each measurement, containing multiple database results.
Key: HQI, Val: Resultname
"""
pass
def applyHQIListToParticleMeasurements(self, hqiList): def applyHQIListToParticleMeasurements(self, hqiList):
'''HQI-List is list of spectra hqis in order of spectra indices''' '''HQI-List is list of spectra hqis in order of spectra indices'''
...@@ -185,12 +192,20 @@ class ParticleContainer(object): ...@@ -185,12 +192,20 @@ class ParticleContainer(object):
particle = self.getParticleOfIndex(partIndex) particle = self.getParticleOfIndex(partIndex)
return particle.getParticleAssignment() return particle.getParticleAssignment()
def getMeasurementPixelCoords(self): def getMeasurementPixelCoords(self) -> list:
coords = [] coords: list = []
for meas in self.measurements: for meas in self.measurements:
coords.append([meas.pixelcoord_x, meas.pixelcoord_y]) coords.append([meas.pixelcoord_x, meas.pixelcoord_y])
return coords return coords
def getApertureCenterCoords(self) -> list:
coords: list = []
for particle in self.particles:
assert particle.aperture is not None, 'Aperture for particle was not set!'
coords.append([particle.aperture.centerX, particle.aperture.centerY])
return coords
def getNumberOfParticlesOfAssignment(self, assignment): def getNumberOfParticlesOfAssignment(self, assignment):
num = 0 num = 0
for particle in self.particles: for particle in self.particles:
......
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GEPARD - Gepard-Enabled PARticle Detection
Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für
Polymerforschung Dresden e. V. <bittrich-lars@ipfdd.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program, see COPYING.
If not, see <https://www.gnu.org/licenses/>.
"""
import numpy as np