# -*- coding: utf-8 -*- """ GEPARD - Gepard-Enabled PARticle Detection Copyright (C) 2018 Lars Bittrich and Josef Brandt, Leibniz-Institut für Polymerforschung Dresden e. V. 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 . """ import numpy as np from PyQt5 import QtCore, QtWidgets, QtGui from segmentation import Segmentation from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg import matplotlib.pyplot as plt from threading import Thread Nscreen = 1000 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 and event.modifiers() & QtCore.Qt.ControlModifier: 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() if self.drag=="add": self.seedpoints.append([p0.x(),p0.y(),self.seedradius]) elif self.drag=="delete": self.seeddeletepoints.append([p0.x(),p0.y(),self.seedradius]) else: self.removeSeeds([p0.x(),p0.y()]) self.update() super().mousePressEvent(event) def mouseMoveEvent(self, event): if self.drag: p0 = event.pos() if self.drag=="add": self.seedpoints.append([p0.x(),p0.y(),self.seedradius]) elif self.drag=="delete": self.seeddeletepoints.append([p0.x(),p0.y(),self.seedradius]) else: self.removeSeeds([p0.x(),p0.y()]) self.update() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if self.drag: self.seedChanged.emit() self.drag = False super().mouseReleaseEvent(event) def clearData(self): self.contours = [] self.measpoints = [] self.seedpoints = [] self.seeddeletepoints = [] self.overlay = None def updateSeedPoints(self, seedpoints=[], seeddeletepoints=[]): # if len(seedpoints) > 0 and len(self.seedpoints) > 0: # print(seedpoints[0, :], self.seedpoints[0, :]) # else: # print('else...', len(seedpoints), len(self.seedpoints)) 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 p in self.measpoints: painter.drawEllipse(p[0]-2, p[1]-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, img, dataset, parent=None): super().__init__(parent, QtCore.Qt.Window) self.dataset = self.verifySeedpoints(dataset) self.img = img self.imgclip = 0,0,0,0 self.seg = Segmentation(self.dataset) self.thread = None self.view = parent 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() group = QtWidgets.QGroupBox("Detection settings", self) grid = QtWidgets.QGridLayout() self.parameters = [] # create editable parameters: for i, p in enumerate(self.seg.parlist): label, colstretch = None, 1 if p.name == "points": 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.setChecked(p.value) valuefunc = makeValueLambda(paramui.isChecked) colstretch = 2 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) 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]) 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) 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("Use Ctrl for seeds and Ctrl+Shift for delete points"), i+3, 0, 1, 2, QtCore.Qt.AlignLeft) grid.addWidget(QtWidgets.QLabel("Ctrl+Alt removes seeds near cursor"), i+4, 0, 1, 2, QtCore.Qt.AlignLeft) group.setLayout(grid) vbox.addWidget(group) 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) 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 saveDetectParams(self, ds=None): if ds is not None: for param in self.parameters: if param[1] == 'points': 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: #Josef says: I replaced the commented logic with the one right here below, as the old one somehow did not work.... The for-loop might become slow at some point?? 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 for point in arr2: #Josef says: I replaced the commented logic with the one right here below, as the old one somehow did not work.... The for-loop might become slow at some point?? if point[0] > (m1-point[2]) and point[0] <= (m2+point[2]) and point[1] > (n1-point[2]) and point[1] <= (n2+point[2]): seeddeletepoints.append([point[0] - p0[0][0], point[1] - p0[0][1], point[2]]) self.imglabel.updateSeedPoints(seedpoints, seeddeletepoints) def closeEvent(self, event): self.detectionFinished.emit() # self.destroy() def mousePressEvent(self, event): if event.button()==QtCore.Qt.RightButton: self.drag = True self.p0 = event.pos() def mouseMoveEvent(self, event): if self.drag: dp = self.p0-event.pos() self.lastmove = (self.lastcenter[0]+dp.x(),self.lastcenter[1]+dp.y()) self.setImageCenter(self.lastmove) def wheelEvent(self, event): if event.angleDelta().y() > 0: self.seedradiusedit.setValue(self.seedradiusedit.value()+1) else: self.seedradiusedit.setValue(self.seedradiusedit.value()-1) def mouseReleaseEvent(self, event): if self.drag: self.lastcenter = self.lastmove self.drag = False def setDataSet(self, ds): self.dataset = ds self.updateImageSeeds() def setImageCenter(self, center=None): if center is None: centerx = self.img.shape[1]//2 centery = self.img.shape[0]//2 else: centerx, centery = center if self.img.shape[0]self.img.shape[0]: n1 -= n2-self.img.shape[0] if n1<0: n1 = 0 n2 = self.img.shape[0] if m1<0: m2 -= m1 m1 = 0 if m2>self.img.shape[1]: m1 -= m2-self.img.shape[1] if m1<0: m1 = 0 m2 = self.img.shape[1] sub = self.img[n1:n2,m1:m2,:].copy() self.imgclip = n1,n2,m1,m2 self.subimg = sub height, width, channel = sub.shape bytesPerLine = 3 * width pix = QtGui.QPixmap() pix.convertFromImage(QtGui.QImage(sub.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) self.imglabel.clearData() self.imglabel.setPixmap(pix) self.updateImageSeeds() def detectShow(self, showname): self.saveDetectParams(self.dataset) img = self.subimg.copy() kwargs = {} for ui, name, valuefunc in self.parameters: kwargs[name] = valuefunc() self.seg.setParameters(**kwargs) seedradius = self.seedradiusedit.value() if showname is not None: stepImg, imgtype = self.seg.apply2Image(img, self.imglabel.seedpoints, self.imglabel.seeddeletepoints, seedradius, return_step=showname) self.imglabel.showStep(stepImg, imgtype) else: measurementpoints, contours, particlestats = self.seg.apply2Image(img, self.imglabel.seedpoints, self.imglabel.seeddeletepoints, seedradius) self.imglabel.updateDetectionResults(contours, measurementpoints) @QtCore.pyqtSlot() def clearDetection(self): if self.dataset is not None: self.dataset.ramanpoints = [] self.dataset.particlecontours = [] self.dataset.particlestats = [] self.dataset.ramanscansortindex = [] self.dataset.ramanscandone = False self.dataset.mode = "opticalscan" self.dataset.save() self.imageUpdate.emit(self.view.microscopeMode) @QtCore.pyqtSlot() def cancelThread(self): self.seg.cancelcomputation = True self.thread.join() self.seg.cancelcomputation = False self.threadrunning = False def blockUI(self): self.pdetectsub.setEnabled(False) self.pclear.setEnabled(False) def unBlockUI(self): self.pdetectsub.setEnabled(True) self.pclear.setEnabled(True) @QtCore.pyqtSlot() def detectParticles(self): self.saveDetectParams(self.dataset) if self.thread is not None and self.thread.is_alive(): self.cancelThread() return self.blockUI() self.pdetectall.setText("Cancel") self.threadrunning = True self.thread = Thread(target=self._worker) self.thread.start() self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.checkOnComputation) self.timer.setSingleShot(True) self.timer.start(100.) @QtCore.pyqtSlot() def checkOnComputation(self): if self.thread is not None: if not self.threadrunning: #self.thread.join() self.thread = None self.unBlockUI() self.pdetectall.setText("Detect all") self.imageUpdate.emit(self.view.microscopeMode) if self.dataset is not None: self.setWindowTitle(str(len(self.dataset.ramanpoints)) + " Particles") else: self.timer.start(100.) def _worker(self): kwargs = {} seedpoints, deletepoints = [], [] if self.dataset is not None: seedpoints = self.dataset.seedpoints deletepoints = self.dataset.seeddeletepoints for ui, name, valuefunc in self.parameters: kwargs[name] = valuefunc() seedradius = self.seedradiusedit.value() self.seg.setParameters(**kwargs) measurementpoints, contours, particlestats = self.seg.apply2Image(self.img, seedpoints, deletepoints, seedradius) if measurementpoints is None: # computation was canceled return if self.dataset is not None: self.dataset.ramanscandone = False self.dataset.ramanpoints = measurementpoints self.dataset.particlecontours = contours self.dataset.particlestats = particlestats self.dataset.ramanscansortindex = [] self.dataset.mode = "prepareraman" self.dataset.save() self.threadrunning = False if __name__ == "__main__": import cv2 import sys from helperfunctions import cv2imread_fix from dataset import DataSet from time import time fname = r"C:\Users\brandt\Desktop\20180723DemoTZW\fullimage.tif" app = QtWidgets.QApplication(sys.argv) t2 = time() img = cv2imread_fix(fname) t3 = time() print("OpenCV read:", t3-t2) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) ds = DataSet("dummy") view = ParticleDetectionView(img, ds, None) view.setDataSet(ds) view.show() app.exec_()