Commit eeaa9d71 authored by JosefBrandt's avatar JosefBrandt

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.
parent 690e2849
......@@ -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
......
......@@ -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.)
......
......@@ -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.)
......
......@@ -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
......
#!/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/>.
"""
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
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment