Commit 5e79c241 authored by Josef Brandt's avatar Josef Brandt

Automated Test until Spectrum Scan

parent 31eb4bb9
......@@ -79,7 +79,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
closeAll()
@QtCore.pyqtSlot(float)
def scalingChanged(self, scale):
def scalingChanged(self):
self.zoomInAct.setEnabled(self.view.scaleFactor < 20.0)
self.zoomOutAct.setEnabled(self.view.scaleFactor > .01)
self.normalSizeAct.setEnabled(self.view.scaleFactor != 1.)
......@@ -91,7 +91,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
if fileName:
self.fname = str(fileName)
self.view.open(self.fname)
self.scalingChanged(1.)
self.scalingChanged()
@QtCore.pyqtSlot()
def importProject(self, fileName=False):
......@@ -101,7 +101,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
if fileName:
self.fname = str(fileName)
self.view.importProject(self.fname)
self.scalingChanged(1.)
self.scalingChanged()
@QtCore.pyqtSlot()
def new(self, fileName=False):
......@@ -113,7 +113,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
if isValid:
self.fname = str(fileName)
self.view.new(self.fname)
self.scalingChanged(1.)
self.scalingChanged()
else:
QtWidgets.QMessageBox.critical(self, "Error", msg)
......
# -*- 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
import os
import logging
import logging.handlers
from PyQt5 import QtCore, QtWidgets, QtGui
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
import matplotlib.pyplot as plt
from threading import Thread
# from win32api import GetSystemMetrics # I use that to find out screen resolution. Might not work on Linux that way??
from .segmentation import Segmentation
from .analysis.particleCharacterization import getParticleStatsWithPixelScale, loadZValImageFromDataset
from .errors import InvalidParticleError, showErrorMessageAsWidget
from .uielements import TimeEstimateProgressbar
from .scenePyramid import ScenePyramid
from .gepardlogging import setDefaultLoggingConfig
from .helperfunctions import hasFTIRControl
def getScreenHeight() -> int:
app = QtWidgets.QApplication.instance()
screen_resolution: QtCore.QRect = app.desktop().screenGeometry()
screenHeight = screen_resolution.height()
Nscreen = np.clip(1000, 0, round(screenHeight * 0.9)) # account for too small displays
return Nscreen
class HistWidget(QtWidgets.QWidget):
def __init__(self, histcallback, curvecallback, parent=None):
super().__init__(parent)
self.updateCallbacks(histcallback, curvecallback)
if parent.dataset is None:
self.points = np.array([[50,0],[100,220],[200,255]])
else:
self.points = parent.dataset.detectParams['points']
self.fig = plt.Figure()
self.canvas = FigureCanvasQTAgg(self.fig)
self.canvas.setParent(self)
self.ax = self.fig.add_subplot(111, autoscale_on=False)
self.ax.axis("off")
self.fig.subplots_adjust(left=0.05, top=0.98, bottom=0.05, right=0.995)
self.canvas.mpl_connect('pick_event', self.moveControlPoint)
self.canvas.mpl_connect('button_release_event', self.releaseAxes)
self.canvas.mpl_connect('motion_notify_event', self.moveAxes)
self.moveind = None
self.plot()
self.setMaximumSize(200,200)
vbox = QtWidgets.QVBoxLayout()
vbox.addWidget(self.canvas)
self.setLayout(vbox)
def value(self):
return self.points
def moveAxes(self, event):
if (event.inaxes==self.ax) and self.moveind is not None:
x = event.xdata
y = event.ydata
if self.moveind == 0:
self.points[0,0] = max(0,min(x, self.points[1,0]))
elif self.moveind == 1:
self.points[1,0] = max(self.points[0,0]+1, min(x, self.points[2,0]-1))
self.points[1,1] = max(0, min(y, 255))
else:
self.points[2,0] = max(self.points[1,0]+1, min(x, 255))
self.plot()
def releaseAxes(self, event):
self.moveind = None
def plot(self):
self.ax.lines = []
self.ax.collections = []
self.ax.artists = []
hist = self.histcallback()[:,0]
hist /= hist.max()
hist *= 255
hist[0] = 0
hist[-1] = 0
xarr, arr = self.curvecallback(self.points)
self.ax.fill(xarr, hist, color=(.7,.7,.7))
self.ax.plot([0,0], [255,255], "k-")
self.ax.plot(xarr, arr, "b-")
self.ax.plot(self.points[:,0], self.points[:,1], "go", picker=5)
self.ax.set_xlim(0,255)
self.ax.set_ylim(0,255)
self.canvas.draw()
self.repaint()
def updateCallbacks(self, histcallback, curvecallback):
self.histcallback = histcallback
self.curvecallback = curvecallback
def moveControlPoint(self, event):
if self.moveind is None:
self.moveind = event.ind
class ImageView(QtWidgets.QLabel):
seedChanged = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent)
self.overlay = None
self.drag = False
self.alpha = .8
self.contours = []
self.seedradius = 3
self.showseedpoints = True
self.measpoints = []
self.seedpoints = []
self.seeddeletepoints = []
@QtCore.pyqtSlot(int)
def resetAlpha(self, num):
self.alpha = num/100.
self.update()
@QtCore.pyqtSlot(int)
def setSeedRadius(self, num):
self.seedradius = num
self.update()
@QtCore.pyqtSlot(int)
def changeSeedDisplay(self, state):
self.showseedpoints = (state==QtCore.Qt.Checked)
self.update()
def showStep(self, img, imgtype):
self.overlay = None
if imgtype==0:
img = img.repeat(3).reshape(img.shape[0],img.shape[1],3)
self.setOverlay(img)
elif imgtype==1:
levels = img
img = np.zeros(tuple(levels.shape)+(3,), dtype=np.uint8)
img[:,:,0][np.nonzero(levels&1)] = 255.
img[:,:,1][np.nonzero(levels&2)] = 255.
self.setOverlay(img)
self.update()
def removeSeeds(self,p0):
p0 = np.array([p0])
if len(self.seedpoints)>0:
arr = np.array(self.seedpoints)
d = np.linalg.norm(arr[:, :2]-p0, axis=1)
ind = d>self.seedradius
self.seedpoints = arr[ind,:].tolist()
if len(self.seeddeletepoints)>0:
arr = np.array(self.seeddeletepoints)
d = np.linalg.norm(arr[:, :2]-p0, axis=1)
ind = d>self.seedradius
self.seeddeletepoints = arr[ind,:].tolist()
def mousePressEvent(self, event):
if event.button()==QtCore.Qt.LeftButton:
if event.modifiers()&QtCore.Qt.ShiftModifier:
self.drag = "delete"
elif event.modifiers()&QtCore.Qt.AltModifier:
self.drag = "remove"
else:
self.drag = "add"
p0 = event.pos()
self.appendSeedPoints(p0)
self.update()
super().mousePressEvent(event)
def mouseMoveEvent(self, event):
if self.drag:
p0 = event.pos()
self.appendSeedPoints(p0)
self.update()
super().mouseMoveEvent(event)
def mouseReleaseEvent(self, event):
if self.drag:
self.seedChanged.emit()
self.drag = False
super().mouseReleaseEvent(event)
def appendSeedPoints(self, pos):
Nscreen = getScreenHeight()
if 0 <= pos.x() < Nscreen and 0 <= pos.y() < Nscreen:
print(pos)
if self.drag == "add":
self.seedpoints.append([pos.x(), pos.y(), self.seedradius])
elif self.drag == "delete":
self.seeddeletepoints.append([pos.x(), pos.y(), self.seedradius])
elif self.drag == "remove":
self.removeSeeds([pos.x(), pos.y()])
def clearData(self):
self.contours = []
self.measpoints = []
self.seedpoints = []
self.seeddeletepoints = []
self.overlay = None
def updateSeedPoints(self, seedpoints=[], seeddeletepoints=[]):
self.seedpoints = seedpoints
self.seeddeletepoints = seeddeletepoints
def updateDetectionResults(self, contours, measpoints):
self.overlay = None
cp = []
for c in contours:
polygon = QtGui.QPolygonF()
for ci in c:
polygon.append(QtCore.QPointF(ci[0,0],ci[0,1]))
cp.append(polygon)
self.contours = cp
self.measpoints = measpoints
self.update()
def setOverlay(self, img):
height, width, channel = img.shape
assert channel==3
bytesPerLine = 3 * width
pix = QtGui.QPixmap()
pix.convertFromImage(QtGui.QImage(img.data, width, height,
bytesPerLine, QtGui.QImage.Format_RGB888))
self.overlay = pix
def paintEvent(self, event):
painter = QtGui.QPainter(self)
painter.drawPixmap(0, 0, self.pixmap())
painter.setOpacity(self.alpha)
if self.overlay is not None:
painter.drawPixmap(0, 0, self.overlay)
else:
painter.setPen(QtCore.Qt.blue)
painter.setBrush(QtCore.Qt.green)
for c in self.contours:
painter.drawPolygon(c)
painter.setPen(QtCore.Qt.red)
painter.setBrush(QtCore.Qt.red)
for points in self.measpoints:
for point in points:
painter.drawEllipse(point.x-2, point.y-2, 5, 5)
if self.showseedpoints:
painter.setPen(QtCore.Qt.white)
painter.setBrush(QtCore.Qt.white)
for p in self.seedpoints:
painter.drawEllipse(p[0]-p[2], p[1]-p[2], 2*p[2], 2*p[2])
painter.setPen(QtCore.Qt.magenta) #I think it is more useful when the deletpoints override the seedpoints
painter.setBrush(QtCore.Qt.magenta)
for p in self.seeddeletepoints:
painter.drawEllipse(p[0]-p[2], p[1]-p[2], 2*p[2], 2*p[2])
class ParticleDetectionView(QtWidgets.QWidget):
imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field)
detectionFinished = QtCore.pyqtSignal(name='detectionFinished')
def __init__(self, pyramid: ScenePyramid, dataset, parent=None):
super().__init__(parent, QtCore.Qt.Window)
self.dataset = self.verifySeedpoints(dataset)
self.pyramid = pyramid
self.img = None
self.imgclip = 0, 0, 0, 0
self.seg = Segmentation(self.dataset, self)
self.seg.detectionState.connect(self.updateDetectionState)
self.thread = None
self.threadrunning = False
self.view = parent
logPath = os.path.join(self.dataset.path, 'detectionlog.txt')
self.logger = logging.getLogger('detection')
self.logger.addHandler(
logging.handlers.RotatingFileHandler(
logPath, maxBytes=5*(1 << 20), backupCount=10)
)
setDefaultLoggingConfig(self.logger)
vbox = QtWidgets.QVBoxLayout()
hbox = QtWidgets.QHBoxLayout()
def makeShowLambda(name):
def f(): return self.detectShow(name)
return QtCore.pyqtSlot()(f)
def makeValueLambda(objmethod):
return lambda : objmethod()
self.imglabel = ImageView(self)
self.imglabel.seedChanged.connect(self.seedChanged)
self.drag = False
self.seedradiusedit = QtWidgets.QSpinBox(self)
self.seedradiusedit.setMinimum(1)
self.seedradiusedit.valueChanged.connect(self.imglabel.setSeedRadius)
self.seedradiusedit.valueChanged.connect(self.updateImageSeeds)
self.seedradiusedit.setValue(3)
self.showseedpoints = QtWidgets.QCheckBox("Show manual seed points", self)
self.showseedpoints.stateChanged.connect(self.imglabel.changeSeedDisplay)
self.showseedpoints.setChecked(True)
self.setImageCenter()
self.globalScaleFactor = QtWidgets.QDoubleSpinBox(self)
self.globalScaleLabel = QtWidgets.QLabel('')
self.globalScaleFactor.valueChanged.connect(self.updatePixelScaleLabel)
self.globalScaleFactor.setMinimum(0.01)
self.globalScaleFactor.setMaximum(1.)
self.globalScaleFactor.setSingleStep(0.1)
self.globalScaleFactor.setDecimals(2)
self.globalScaleFactor.setValue(1.)
self.globalScaleFactor.valueChanged.connect(self.autoUpdateIfDesired)
globalScaleValueFunc = makeValueLambda(self.globalScaleFactor.value)
self.detectParamsGroup = QtWidgets.QGroupBox("Detection settings", self)
grid = QtWidgets.QGridLayout()
self.parameters = []
self.parameters.append([self.globalScaleFactor, 'globalScaleFactor', globalScaleValueFunc, None])
checkBoxesToLink = {}
grid.addWidget(self.globalScaleLabel, 0, 0, QtCore.Qt.AlignLeft)
grid.addWidget(self.globalScaleFactor, 0, 1, QtCore.Qt.AlignRight)
# create editable parameters:
for i, p in enumerate(self.seg.parlist):
i += 1
label, colstretch = None, 1
if p.name == "contrastCurve":
paramui = HistWidget(lambda : self.seg.calculateHist(self.seg.convert2Gray(self.subimg)),
self.seg.calculateHistFunction, self)
valuefunc = makeValueLambda(paramui.value)
colstretch = 2
if p.dtype == np.bool:
paramui = QtWidgets.QCheckBox(p.helptext, self)
paramui.stateChanged.connect(self.autoUpdateIfDesired)
paramui.setChecked(p.value)
valuefunc = makeValueLambda(paramui.isChecked)
colstretch = 2
if p.linkedParameter is not None:
checkBoxesToLink[paramui] = p.linkedParameter
elif p.dtype == int or p.dtype == float:
label = QtWidgets.QLabel(p.helptext, self)
if p.dtype == int:
paramui = QtWidgets.QSpinBox(self)
else:
paramui = QtWidgets.QDoubleSpinBox(self)
paramui.setDecimals(p.decimals)
if p.valrange[0] is not None:
paramui.setMinimum(p.valrange[0])
if p.valrange[1] is not None:
paramui.setMaximum(p.valrange[1])
paramui.setSingleStep(p.stepsize)
paramui.setValue(p.value)
paramui.setMinimumWidth(70)
paramui.valueChanged.connect(self.autoUpdateIfDesired)
valuefunc = makeValueLambda(paramui.value)
elif p.dtype is None:
label = QtWidgets.QLabel(p.helptext, self)
paramui = None
if paramui is not None:
self.parameters.append([paramui, p.name, valuefunc, None])
if colstretch == 1:
grid.addWidget(paramui, i, 1, QtCore.Qt.AlignLeft)
else:
grid.addWidget(paramui, i, 0, 1, 2, QtCore.Qt.AlignLeft)
if label is not None:
grid.addWidget(label, i, 0, QtCore.Qt.AlignLeft)
if p.show is True:
pshow = QtWidgets.QPushButton(">", self)
pshow.released.connect(makeShowLambda(p.name))
grid.addWidget(pshow, i, 2, QtCore.Qt.AlignRight)
if paramui is not None:
self.parameters[-1][3] = pshow
#link checkboxes to other parameters:
def makeEnableLambda(checkbox, parameter):
return lambda: parameter.setEnabled(checkbox.isChecked())
for box in checkBoxesToLink:
linkedName = checkBoxesToLink[box]
for p in self.parameters:
if p[1] == linkedName:
p[0].setEnabled(box.isChecked())
box.stateChanged.connect(makeEnableLambda(box, p[0])) #the actual control element
if p[3] is not None: #the "show" box, if present
box.stateChanged.connect(makeEnableLambda(box, p[3]))
p[3].setEnabled(box.isChecked())
label = QtWidgets.QLabel("Seed radius", self)
grid.addWidget(label, i+1, 0, QtCore.Qt.AlignLeft)
grid.addWidget(self.seedradiusedit, i+1, 1, QtCore.Qt.AlignLeft)
grid.addWidget(self.showseedpoints, i+2, 0, 1, 2, QtCore.Qt.AlignLeft)
grid.addWidget(QtWidgets.QLabel("Click mouse to add seeds, Click+Shift to add deletepoints"), i+3, 0, 1, 2, QtCore.Qt.AlignLeft)
grid.addWidget(QtWidgets.QLabel("Click+Alt removes seeds near cursor"), i+4, 0, 1, 2, QtCore.Qt.AlignLeft)
self.detectParamsGroup.setLayout(grid)
paramGroupScrollArea = QtWidgets.QScrollArea(self)
Nscreen = getScreenHeight()
maxHeight = np.clip(1000, 0, Nscreen*0.8)
paramGroupScrollArea.setFixedHeight(maxHeight)
paramGroupScrollArea.setWidget(self.detectParamsGroup)
vbox.addWidget(paramGroupScrollArea)
self.updateSeedsInSampleViewBtn = QtWidgets.QPushButton("Update Seedpoints in fullimage view", self)
self.updateSeedsInSampleViewBtn.released.connect(self.updateSeedsInSampleview)
self.hideSeedsInSampleViewBtn = QtWidgets.QPushButton("Hide Seedpoints in fullimage view", self)
self.hideSeedsInSampleViewBtn.released.connect(self.removeSeedsInSampleView)
vbox.addWidget(self.updateSeedsInSampleViewBtn)
vbox.addWidget(self.hideSeedsInSampleViewBtn)
self.slider = QtWidgets.QSlider(self)
self.slider.setRange(0,100)
self.slider.setValue(80)
self.slider.setOrientation(QtCore.Qt.Horizontal)
self.slider.sliderMoved.connect(self.imglabel.resetAlpha)
vbox.addWidget(self.slider)
self.autoUpdateCheckBox = QtWidgets.QCheckBox('Auto-update detection')
self.autoUpdateCheckBox.setMaximumWidth(200)
self.autoUpdateCheckBox.setChecked(True)
vbox.addWidget(self.autoUpdateCheckBox)
self.progressbar = TimeEstimateProgressbar()
vbox.addWidget(self.progressbar)
hbox2 = QtWidgets.QHBoxLayout()
self.pdetectsub = QtWidgets.QPushButton("Detect", self)
self.pdetectall = QtWidgets.QPushButton("Detect all", self)
self.pclear = QtWidgets.QPushButton("Clear detection", self)
self.pdetectsub.released.connect(makeShowLambda(None))
self.pdetectall.released.connect(self.detectParticles)
self.pclear.released.connect(self.clearDetection)
hbox2.addWidget(self.pdetectsub)
hbox2.addWidget(self.pdetectall)
hbox2.addWidget(self.pclear)
vbox.addLayout(hbox2)
vbox.addStretch()
hbox.addLayout(vbox,0)
hbox.addWidget(self.imglabel,1)
self.setLayout(hbox)
self.setWindowTitle("Particle Detection")
def updatePixelScaleLabel(self):
newScale = self.dataset.getPixelScale(self.dataset.imagescanMode) / self.globalScaleFactor.value()
newScale = np.round(newScale, 2)
self.globalScaleLabel.setText(f'Scale for image (PixelScale: {newScale} µm/px)')
def saveDetectParams(self, ds=None):
if ds is not None:
for param in self.parameters:
if param[1] == 'contrastCurve':
print(param[0].value())
try: # is it a spinbox or the histWidget? Read out the value
ds.detectParams[param[1]] = param[0].value()
except: # otherwise checkbox -> take its state
ds.detectParams[param[1]] = param[0].isChecked()
ds.detectParams['seedRad'] = self.seedradiusedit.value()
ds.save()
def verifySeedpoints(self, dataset):
seedpoints = dataset.seedpoints
seeddeletepoints = dataset.seeddeletepoints
if len(seedpoints) > 0: # points are present
if seedpoints.shape[1] == 2: # old entries with only x,y coordinates
radii = np.ones((seedpoints.shape[0], 1))*3
dataset.seedpoints = np.hstack((seedpoints, radii))
else:
dataset.seedpoints = np.array([])
if len(seeddeletepoints) > 0: # points are present
if seeddeletepoints.shape[1] == 2: #old entries with only x,y coordinates
radii = np.ones((seeddeletepoints.shape[0], 1))*3
dataset.seeddeletepoints = np.hstack((seeddeletepoints, radii))
else:
dataset.seeddeletepoints = np.array([])
return dataset
@QtCore.pyqtSlot()
def seedChanged(self):
seedradius = self.seedradiusedit.value()
def clipdata(arr0, arr1, p0, n1, n2, m1, m2):
if arr0.shape[0]>0:
ind = (arr0[:,0]>m1-seedradius)&(arr0[:,0]<=m2+seedradius)
ind &= (arr0[:,1]>n1-seedradius)&(arr0[:,1]<=n2+seedradius)
arr0 = arr0[~ind,:]
else:
arr0 = arr0.reshape(0,3)
if arr1.shape[0]>0:
arr1[:,:2] += p0
else:
arr1 = arr1.reshape(0,3)
return np.concatenate((arr0, arr1), axis=0)
if self.dataset is not None:
n1,n2,m1,m2 = self.imgclip
p0 = np.array([[m1,n1]], dtype=np.int32)
self.dataset.seedpoints = clipdata(self.dataset.seedpoints,
np.array(self.imglabel.seedpoints, dtype=np.int32),
p0, n1, n2, m1, m2)
self.dataset.seeddeletepoints = clipdata(self.dataset.seeddeletepoints,
np.array(self.imglabel.seeddeletepoints, dtype=np.int32),
p0, n1, n2, m1, m2)
self.dataset.save()
@QtCore.pyqtSlot()
def updateImageSeeds(self):
if self.dataset is not None:
n1,n2,m1,m2 = self.imgclip
p0 = np.array([[m1,n1]], dtype=np.int32)
seedpoints = []
seeddeletepoints = []
arr1 = self.dataset.seedpoints
# what seeds are actually in image view?
for point in arr1:
if point[0] > (m1-point[2]) and point[0] <= (m2+point[2]) and point[1] > (n1-point[2]) and point[1] <= (n2+point[2]):
seedpoints.append([point[0] - p0[0][0], point[1] - p0[0][1], point[2]])
arr2 = self.dataset.seeddeletepoints