From eeaa9d71e6fb6e54767d3fd5f8c92f3f4e016545 Mon Sep 17 00:00:00 2001 From: JosefBrandt Date: Mon, 2 Dec 2019 10:18:29 +0100 Subject: [PATCH] DownScaling of too large connectedComponents, Progressbar A new parameter allows setting a maximum size of connected Components. Larger components are scaled down accordingly to avoid MemoryErrors. A progress bar is implemented for visualizing the detection progress. The progress bar is put in a dedicated class (in uielements.py) and it is now also used by opticalscan and ramanscanui. --- detectionview.py | 32 +++++++++++++- opticalscan.py | 29 ++++--------- ramanscanui.py | 31 +++++-------- segmentation.py | 77 +++++++++++++++++++++++++------- uielements.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 224 insertions(+), 56 deletions(-) create mode 100644 uielements.py diff --git a/detectionview.py b/detectionview.py index 320a457..417348e 100644 --- a/detectionview.py +++ b/detectionview.py @@ -27,6 +27,7 @@ from threading import Thread from .segmentation import Segmentation from .analysis.particleCharacterization import getParticleStatsWithPixelScale, loadZValImageFromDataset from .errors import InvalidParticleError +from .uielements import TimeEstimateProgressbar Nscreen = 1000 @@ -288,6 +289,7 @@ class ParticleDetectionView(QtWidgets.QWidget): self.img = img self.imgclip = 0,0,0,0 self.seg = Segmentation(self.dataset, self) + self.seg.detectionState.connect(self.updateDetectionState) self.thread = None self.view = parent @@ -417,6 +419,9 @@ class ParticleDetectionView(QtWidgets.QWidget): 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) @@ -642,6 +647,10 @@ class ParticleDetectionView(QtWidgets.QWidget): @QtCore.pyqtSlot() def detectParticles(self): + """ + Detect all particles + :return: + """ self.saveDetectParams(self.dataset) if self.thread is not None and self.thread.is_alive(): self.cancelThread() @@ -661,6 +670,7 @@ class ParticleDetectionView(QtWidgets.QWidget): if self.thread is not None: if not self.threadrunning: self.thread = None + self.progressbar.disable() self.unBlockUI() self.pdetectall.setText("Detect all") self.imageUpdate.emit(self.view.microscopeMode) @@ -670,6 +680,25 @@ class ParticleDetectionView(QtWidgets.QWidget): self.setWindowTitle(f'{numParticles} Particles ({numMeasurements} Measurements)') else: self.timer.start(100.) + + @QtCore.pyqtSlot(str) + def updateDetectionState(self, message): + """ + Updates the progressbar and its text-label to the current state of the detection + :return: + """ + if message.find('DO') == -1: + self.progressbar.setMessage(message) + else: + if message.find('setup') != -1: + self.progressbar.resetTimerAndCounter() + self.progressbar.enable() + elif message.find('maxVal') != -1: + maxVal = int(message.split('=')[-1]) + self.progressbar.setMaxValue(maxVal) + elif message.find('newVal') != -1: + newVal = int(message.split('=')[-1]) + self.progressbar.setValue(newVal) def _worker(self): kwargs = {} @@ -681,8 +710,9 @@ class ParticleDetectionView(QtWidgets.QWidget): kwargs[name] = valuefunc() seedradius = self.seedradiusedit.value() self.seg.setParameters(**kwargs) - + measurementPoints, contours= self.seg.apply2Image(self.img, seedpoints, deletepoints, seedradius, self.dataset) + if measurementPoints is None: # computation was canceled return diff --git a/opticalscan.py b/opticalscan.py index 20c94c9..f1ec6b2 100644 --- a/opticalscan.py +++ b/opticalscan.py @@ -28,9 +28,9 @@ import os import cv2 from .helperfunctions import cv2imread_fix, cv2imwrite_fix from time import time -import datetime import sys from .opticalbackground import BackGroundManager +from .uielements import TimeEstimateProgressbar def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, logpath='', ishdr=False): @@ -289,11 +289,8 @@ class OpticalScan(QtWidgets.QWidget): self.pexit.released.connect(self.stopScan) self.prun.setEnabled(False) - self.timelabeltext = "Estimated time to finish: " - self.progressbar = QtWidgets.QProgressBar(self) - self.progresstime = QtWidgets.QLabel(self.timelabeltext, self) - self.progresstime.setEnabled(False) - self.progressbar.setEnabled(False) + self.progressbar = TimeEstimateProgressbar() + self.progressbar.disable() radioGroup = QtWidgets.QGroupBox('Shape') radioLayout = QtWidgets.QHBoxLayout() @@ -349,7 +346,6 @@ class OpticalScan(QtWidgets.QWidget): vbox.addWidget(self.areaOptionsGroup) vbox.addWidget(furtherOptionsGroup) vbox.addLayout(btnLayout) - vbox.addWidget(self.progresstime) vbox.addWidget(self.progressbar) self.setLayout(vbox) @@ -382,6 +378,7 @@ class OpticalScan(QtWidgets.QWidget): QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: + self.progressbar.resetTimerAndCounter() self.timer.stop() self.processstopevent.set() self.process.join() @@ -603,11 +600,9 @@ class OpticalScan(QtWidgets.QWidget): self.dataqueue, self.processstopevent, self.logpath, self.hdrcheck.isChecked())) self.process.start() - self.starttime = time() - self.progresstime.setEnabled(True) - self.progressbar.setEnabled(True) - self.progressbar.setRange(0, len(self.dataset.grid)) - self.progressbar.setValue(0) + self.progressbar.enable() + self.progressbar.resetTimerAndCounter() + self.progressbar.setMaxValue(len(self.dataset.grid)) self.view.imgdata = None self.view.blockUI() grid = np.asarray(self.dataset.grid) @@ -647,11 +642,6 @@ class OpticalScan(QtWidgets.QWidget): self.view.imgdata, self.dataset.zvalimg = loadAndPasteImage(names, self.view.imgdata, self.dataset.zvalimg, width, height, rotationvalue, p0, p1, p, background=background_img) self.progressbar.setValue(i+1) - if i>3: - timerunning = time()-self.starttime - ttot = timerunning*Ngrid/(i+1) - time2go = ttot - timerunning - self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) self.imageUpdate.emit(self.view.microscopeMode) if i==Ngrid-1: @@ -672,9 +662,8 @@ class OpticalScan(QtWidgets.QWidget): self.view.saveDataSet() self.view.unblockUI() self.view.switchMode("ParticleDetection") - self.progressbar.setValue(0) - self.progressbar.setEnabled(False) - self.progresstime.setEnabled(False) + self.progressbar.resetTimerAndCounter() + self.progressbar.disable() self.close() return self.timer.start(100.) diff --git a/ramanscanui.py b/ramanscanui.py index 62dedb2..b18cc5d 100644 --- a/ramanscanui.py +++ b/ramanscanui.py @@ -24,10 +24,10 @@ import numpy as np from multiprocessing import Process, Queue, Event import queue from time import time -from .external import tsp -import datetime import sys import os +from .external import tsp +from .uielements import TimeEstimateProgressbar def reorder(points, N=20): y0, y1 = points[:,1].min(), points[:,1].max() @@ -129,18 +129,15 @@ class RamanScanUI(QtWidgets.QWidget): self.pexit = QtWidgets.QPushButton("Cancel", self) self.pexit.released.connect(self.stopScan) self.prun.setEnabled(False) - self.progressbar = QtWidgets.QProgressBar(self) - self.timelabeltext = "Estimated time to finish: " - self.progresstime = QtWidgets.QLabel(self.timelabeltext, self) - self.progresstime.setEnabled(False) - self.progressbar.setEnabled(False) + + self.progressbar = TimeEstimateProgressbar() + self.progressbar.disable() hbox.addStretch() hbox.addWidget(self.pexit) vbox.addWidget(self.group2) vbox.addLayout(hbox) - vbox.addWidget(self.progresstime) vbox.addWidget(self.progressbar) self.setLayout(vbox) @@ -173,6 +170,7 @@ class RamanScanUI(QtWidgets.QWidget): if reply == QtWidgets.QMessageBox.Yes: self.ramanctrl.finishMeasurement() self.timer.stop() + self.progressbar.resetTimerAndCounter() self.processstopevent.set() self.process.join() self.dataqueue.close() @@ -248,10 +246,9 @@ class RamanScanUI(QtWidgets.QWidget): self.view.highLightRamanIndex(0) self.view.blockUI() self.group2.setEnabled(False) - self.progresstime.setEnabled(True) - self.progressbar.setEnabled(True) - self.progressbar.setRange(0, len(scanpoints)) - self.progressbar.setValue(0) + self.progressbar.enable() + self.progressbar.resetTimerAndCounter() + self.progressbar.setMaxValue(len(scanpoints)) self.ramanctrl.disconnect() self.processstopevent = Event() self.dataqueue = Queue() @@ -279,11 +276,6 @@ class RamanScanUI(QtWidgets.QWidget): self.view.highLightRamanIndex(i+1) #go to next scanmarker Npoints = len(self.dataset.particleContainer.getMeasurementPixelCoords()) - if i>3: - timerunning = time()-self.starttime - ttot = timerunning*Npoints/(i+1) - time2go = ttot - timerunning - self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) if i==Npoints-1: self.process.join() self.dataqueue.close() @@ -291,9 +283,8 @@ class RamanScanUI(QtWidgets.QWidget): self.dataset.ramanscandone = True self.view.saveDataSet() self.view.unblockUI() - self.progressbar.setValue(0) - self.progressbar.setEnabled(False) - self.progresstime.setEnabled(False) + self.progressbar.resetTimerAndCounter() + self.progressbar.disable() self.close() return self.timer.start(100.) diff --git a/segmentation.py b/segmentation.py index 6cb0949..cc75d58 100644 --- a/segmentation.py +++ b/segmentation.py @@ -28,6 +28,8 @@ from skimage.feature import peak_local_max from skimage.morphology import watershed import skfuzzy as fuzz import random +from PyQt5 import QtCore + def closeHolesOfSubImage(subimg): subimg = cv2.copyMakeBorder(subimg, 1, 1, 1, 1, 0) @@ -63,8 +65,10 @@ class MeasurementPoint(object): self.x = x self.y = y -class Segmentation(object): - def __init__(self, dataset=None, parent=None): +class Segmentation(QtCore.QObject): + detectionState = QtCore.pyqtSignal(str) + def __init__(self, dataset=None, parent=None): + super(Segmentation, self).__init__() self.cancelcomputation = False self.parent = parent self.defaultParams = {'adaptiveHistEqu': False, @@ -84,6 +88,7 @@ class Segmentation(object): 'minparticledistance': 20, 'closeBackground': False, 'fuzzycluster': False, + 'maxComponentSize': 20000, 'measurefrac': 1, 'compactness': 0.0, 'seedRad': 3} @@ -115,6 +120,7 @@ class Segmentation(object): Parameter("measurefrac", float, self.detectParams['measurefrac'], 0, 1, 2, stepsize = 0.05, helptext="measure fraction of particles", show=False), Parameter("closeBackground", np.bool, self.detectParams['closeBackground'], helptext="close holes in sure background", show=False), Parameter("fuzzycluster", np.bool, self.detectParams['fuzzycluster'], helptext='Enable Fuzzy Clustering', show=False), + Parameter("maxComponentSize", int, self.detectParams['maxComponentSize'], 100, 1E6, 0, 100, helptext='Maximum size in x or y of connected component.\nLarger components are scaled down accordingly', show=False), Parameter("sure_fg", None, helptext="Show sure foreground", show=True), Parameter("compactness", float, self.detectParams['compactness'], 0, 1, 2, 0.05, helptext="watershed compactness", show=False), Parameter("watershed", None, helptext="Show watershed markers", show=True), @@ -140,8 +146,10 @@ class Segmentation(object): :return: """ t0 = time() + self.detectionState.emit('DO: setup') gray = self.convert2Gray(img) + self.detectionState.emit('finished GrayScale') print("gray") if self.adaptiveHistEqu: @@ -149,6 +157,7 @@ class Segmentation(object): numTilesY = round(img.shape[0]/self.claheTileSize) clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(numTilesY,numTilesX)) gray = clahe.apply(gray) + self.detectionState.emit('finished CLAHE') if return_step=="claheTileSize": return gray, 0 print("adaptive Histogram Adjustment") @@ -158,7 +167,8 @@ class Segmentation(object): if self.activateContrastCurve: xi, arr = self.calculateHistFunction(self.contrastCurve) gray = arr[gray] - print("contrast curve") + print("contrast curve") + self.detectionState.emit('finished Contrast Curve') if self.cancelcomputation: return None, None @@ -171,6 +181,7 @@ class Segmentation(object): del gray if return_step=="blurRadius": return blur, 0 + self.detectionState.emit('finished Blurring') print("blur") if self.cancelcomputation: return None, None @@ -215,6 +226,7 @@ class Segmentation(object): thresh = self.closeBrightHoles(thresh, blur, self.maxholebrightness) del blur print("thresholded") + self.detectionState.emit('finished thresholding') # modify thresh with seedpoints and deletepoints for p in np.int32(seedpoints): @@ -235,6 +247,8 @@ class Segmentation(object): '''the peak_local_max function takes the min distance between peaks. Unfortunately, that means that individual particles smaller than that distance are consequently disregarded. Hence, we need a connectec_components approach''' n, labels, stats, centroids = cv2.connectedComponentsWithStats(thresh, 8, cv2.CV_32S) + self.detectionState.emit('finished connected components search') + self.detectionState.emit(f'DO: maxVal={n-1}') del thresh measurementPoints = {} @@ -249,7 +263,6 @@ class Segmentation(object): else: previewImage = np.zeros(img.shape[:2]) - for label in range(1, n): area = stats[label, cv2.CC_STAT_AREA] if self.minparticlearea < area < maxArea: @@ -257,21 +270,35 @@ class Segmentation(object): left = stats[label, cv2.CC_STAT_LEFT] width = stats[label, cv2.CC_STAT_WIDTH] height = stats[label, cv2.CC_STAT_HEIGHT] + subthresh = np.uint8(255 * (labels[up:(up+height), left:(left+width)] == label)) + + scaleFactor = 1.0 + if width > self.maxComponentSize or height > self.maxComponentSize: + scaleFactor = max([width/self.maxComponentSize, height/self.maxComponentSize]) + subthresh = cv2.resize(subthresh, None, fx=1/scaleFactor, fy=1/scaleFactor) + subdist = cv2.distanceTransform(subthresh, cv2.DIST_L2, 3) - sure_fg = self.getSureForeground(subthresh, subdist, self.minparticledistance) + minDistance = round(self.minparticledistance / scaleFactor) + sure_fg = self.getSureForeground(subthresh, subdist, minDistance) sure_bg = cv2.dilate(subthresh, np.ones((5, 5)), iterations = 1) if self.closeBackground: sure_bg = self.closeHoles(sure_bg) # modify sure_fg and sure_bg with seedpoints and deletepoints for p in np.int32(seedpoints): - cv2.circle(sure_fg, tuple([p[0]-left, p[1]-up]), int(p[2]), 1, -1) - cv2.circle(sure_bg, tuple([p[0]-left, p[1]-up]), int(p[2]), 1, -1) + x = int(round(p[0] / scaleFactor)-left) + y = int(round(p[1] / scaleFactor) - up) + radius = int(round(p[2] / scaleFactor)) + cv2.circle(sure_fg, (x, y), radius, 1, -1) + cv2.circle(sure_bg, (x, y), radius, 1, -1) for p in np.int32(deletepoints): - cv2.circle(sure_fg, tuple([p[0]-left, p[1]-up]), int(p[2]), 0, -1) - cv2.circle(sure_bg, tuple([p[0]-left, p[1]-up]), int(p[2]), 0, -1) + x = int(round(p[0] / scaleFactor) - left) + y = int(round(p[1] / scaleFactor) - up) + radius = int(round(p[2] / scaleFactor)) + cv2.circle(sure_fg, (x, y), radius, 1, -1) + cv2.circle(sure_bg, (x, y), radius, 1, -1) if self.cancelcomputation: return None, None @@ -288,8 +315,12 @@ class Segmentation(object): markers[unknown==255] = 0 markers = ndi.label(sure_fg)[0] - markers = watershed(-subdist, markers, mask=sure_bg, compactness = self.compactness, watershed_line = True) #labels = 0 for background, 1... for particles - + try: + markers = watershed(-subdist, markers, mask=sure_bg, compactness = self.compactness, watershed_line = True) #labels = 0 for background, 1... for particles + except MemoryError: + self.parent.raiseWarning('Segmentation failed due to large connected components.\nPlease reduce maximal connected Component Size.') + return None, None + if self.cancelcomputation: return None, None @@ -307,17 +338,28 @@ class Segmentation(object): tmpcontours = [contours[i] for i in range(len(contours)) if hierarchy[0,i,3]<0] for cnt in tmpcontours: - if cv2.contourArea(cnt) >= self.minparticlearea: - label = markers[cnt[0,0,1],cnt[0,0,0]] - if label==0: + contourArea = cv2.contourArea(cnt) * scaleFactor**2 + if contourArea >= self.minparticlearea: + tmplabel = markers[cnt[0,0,1],cnt[0,0,0]] + if tmplabel ==0: continue x0, x1 = cnt[:,0,0].min(), cnt[:,0,0].max() y0, y1 = cnt[:,0,1].min(), cnt[:,0,1].max() + subimg = (markers[y0:y1+1,x0:x1+1]).copy() - subimg[subimg!=label] = 0 + subimg[subimg!=tmplabel ] = 0 y, x = self.getMeasurementPoints(subimg) + if scaleFactor != 1: + x0 = int(round(x0 * scaleFactor)) + y0 = int(round(y0 * scaleFactor)) + x = [int(round(subX * scaleFactor)) for subX in x] + y = [int(round(subY * scaleFactor)) for subY in y] + for i in range(len(cnt)): + cnt[i][0][0] = int(round(cnt[i][0][0] * scaleFactor)) + cnt[i][0][1] = int(round(cnt[i][0][1] * scaleFactor)) + for i in range(len(cnt)): cnt[i][0][0] += left cnt[i][0][1] += up @@ -328,7 +370,10 @@ class Segmentation(object): for index in range(0, len(x)): newMeasPoint = MeasurementPoint(particleIndex, x[index] + x0 + left, y[index] + y0 + up) measurementPoints[particleIndex].append(newMeasPoint) + particleIndex += 1 + + self.detectionState.emit(f'DO: newVal={label}') if return_step == 'sure_fg': img = np.zeros_like(preview_surefg) @@ -355,6 +400,8 @@ class Segmentation(object): total_time = time()-t0 print('segmentation took', total_time, 'seconds') + total_time = round(total_time, 2) + self.detectionState.emit(f'finished particle detection after {total_time} seconds') return measurementPoints, finalcontours diff --git a/uielements.py b/uielements.py new file mode 100644 index 0000000..4de3496 --- /dev/null +++ b/uielements.py @@ -0,0 +1,111 @@ +#!/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. + +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 . +""" +from PyQt5 import QtWidgets +from time import time +import datetime + +class TimeEstimateProgressbar(QtWidgets.QGroupBox): + """ + A progressbar with time estimate for computationally expensive tasks. It contains the actual progressbar together + with a text label that is updated accordingly. It also estimates the remaining progress time. + Call enable and disable-methods for optically enabling and disabling the widgets. + Call resetTimerAndCounter before the process starts, + Then set the maxValue using the setMaxValue method. + Finally, for pushing an update, use the setValue method. It will update the text label as well. + Furthermore, a text message can be pushed to the text label at any time, using the setMessage method. + :return: + """ + def __init__(self): + super(TimeEstimateProgressbar, self).__init__() + layout = QtWidgets.QVBoxLayout() + self.setLayout(layout) + + self.timelabeltext = "Estimated time to finish: - " + self.progressbar = QtWidgets.QProgressBar(self) + self.progresstime = QtWidgets.QLabel(self.timelabeltext, self) + self.progresstime.setEnabled(False) + self.progressbar.setEnabled(False) + + self.maxVal = None + self.startTime = None + + layout.addWidget(self.progresstime) + layout.addWidget(self.progressbar) + + def setMaxValue(self, maxVal): + """ + Sets the maximal value of the progressbar. Also needed for estimating remaining time. + :return: + """ + self.maxVal = maxVal + self.progressbar.setRange(0, self.maxVal) + + def enable(self): + """ + Enables the UI widgets. + :return: + """ + self.progressbar.setEnabled(True) + self.progresstime.setEnabled(True) + + def disable(self): + """ + Disables the UI widgets. + :return: + """ + self.progressbar.setEnabled(False) + self.progresstime.setEnabled(False) + + def resetTimerAndCounter(self): + """ + Resets the timer and sets the progressbar to value 0 + :return: + """ + self.startTime = time() + self.progressbar.setValue(0) + + def setMessage(self, msg): + """ + Pushes a message to the progressbar's textlabel. + :return: + """ + self.progresstime.setText(msg) + + def setValue(self, newValue): + """ + Sets the value of the progressbar and calls updating the TimeLabel + :return: + """ + self.progressbar.setValue(newValue) + self._updatetTimeLabel() + + def _updatetTimeLabel(self): + """ + Calculates remaining Time and updates the label. + :return: + """ + if self.progressbar.value() > 3: + timerunning = time()-self.startTime + ttot = timerunning*self.maxVal/(self.progressbar.value()+1) + time2go = ttot - timerunning + self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) + \ No newline at end of file -- GitLab