Commit ce63a986 authored by Josef Brandt's avatar Josef Brandt

ModeHandler and FTIR Apertures

parent 893884dd
......@@ -23,13 +23,16 @@ import logging.handlers
import traceback
import os
from io import StringIO
from PyQt5 import QtCore, QtWidgets, QtGui
from PyQt5 import QtCore, QtWidgets, QtGui, QtTest
from .sampleview import SampleView
from .scalebar import ScaleBar
from .ramancom.ramancontrol import defaultPath
from .ramancom.ramanSwitch import RamanSwitch
from .analysis.colorlegend import ColorLegend
from .gepardlogging import setDefaultLoggingConfig
from .workmodes import ModeHandler
from .unittests.test_gepard import testGepard
class GEPARDMainWindow(QtWidgets.QMainWindow):
def __init__(self, logger):
......@@ -63,6 +66,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.createActions()
self.createMenus()
self.createToolBar()
self.modeHandler: ModeHandler = ModeHandler(self)
self.updateModes()
def resizeEvent(self, event):
......@@ -130,12 +134,16 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
def createActions(self):
def testDenGebbard(gebbard):
return lambda : testGepard(gebbard)
fname = os.path.join(os.path.split(__file__)[0],
os.path.join('data', 'brand.png'))
self.aboutAct = QtWidgets.QAction(QtGui.QIcon(fname),
"About Particle Measurment", self)
self.aboutAct.triggered.connect(self.about)
# self.aboutAct.triggered.connect(self.about)
self.aboutAct.triggered.connect(testDenGebbard(self))
self.openAct = QtWidgets.QAction("&Open Project...", self)
self.openAct.setShortcut("Ctrl+O")
self.openAct.triggered.connect(self.open)
......@@ -183,17 +191,17 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.opticalScanAct = QtWidgets.QAction("Optical Scan", self)
self.opticalScanAct.setEnabled(False)
self.opticalScanAct.setCheckable(True)
self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("OpticalScan")))
self.opticalScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.modeHandler.switchMode("OpticalScan")))
self.detectParticleAct = QtWidgets.QAction("Detect Particles", self)
self.detectParticleAct.setEnabled(False)
self.detectParticleAct.setCheckable(True)
self.detectParticleAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("ParticleDetection")))
self.detectParticleAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.modeHandler.switchMode("ParticleDetection")))
self.ramanScanAct = QtWidgets.QAction("Raman Scan", self)
self.ramanScanAct.setEnabled(False)
self.ramanScanAct.setCheckable(True)
self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.view.switchMode("RamanScan")))
self.ramanScanAct.triggered.connect(QtCore.pyqtSlot()(lambda :self.modeHandler.switchMode("SpectrumScan")))
self.snapshotAct = QtWidgets.QAction("Save Screenshot", self)
self.snapshotAct.triggered.connect(self.view.takeScreenshot)
......@@ -227,14 +235,14 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
ose = True
elif maxenabled == "ParticleDetection":
ose, pde = True, True
elif maxenabled == "RamanScan":
elif maxenabled == "SpectrumScan":
ose, pde, rse = True, True, True
if active == "OpticalScan" and ose:
osc = True
elif active == "ParticleDetection" and pde:
pdc = True
elif active == "RamanScan" and rse:
elif active == "SpectrumScan" and rse:
rsc = True
self.opticalScanAct.setEnabled(ose)
......@@ -243,6 +251,15 @@ class GEPARDMainWindow(QtWidgets.QMainWindow):
self.detectParticleAct.setChecked(pdc)
self.ramanScanAct.setEnabled(rse)
self.ramanScanAct.setChecked(rsc)
def activateMaxMode(self, loadnew=False) -> None:
self.modeHandler.activateMaxMode(loadnew)
def getCurrentMode(self) -> str:
mode: str = 'None'
if self.modeHandler.activeMode is not None:
mode = self.modeHandler.activeMode.name
return mode
def unblockUI(self, connected):
self.openAct.setEnabled(True)
......
......@@ -22,18 +22,20 @@ If not, see <https://www.gnu.org/licenses/>.
import numpy as np
from scipy import optimize
import cv2
from ..cythonModules.getMaxRect import findMaxRect
# from ..cythonModules.getMaxRect import findMaxRect # TODO: UNCOMMENT!!!
####################################################
'''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)
......@@ -61,13 +63,15 @@ def getFinalRectangle(angle, img, shiftScaleParam):
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:
......@@ -86,6 +90,7 @@ def addBorderToMakeImageSquare(img):
return img_square, xshift, yshift
def limitImageSize(img, limitSize):
if img.shape[0] > limitSize:
img_small = cv2.resize(img,(limitSize, limitSize),interpolation=0)
......@@ -96,6 +101,7 @@ def limitImageSize(img, limitSize):
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
......@@ -111,13 +117,15 @@ def makeImageOddSized(img):
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)
nx, ny = img.shape
rotationMatrix = cv2.getRotationMatrix2D(((nx-1)/2, (ny-1)/2), float(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)
......@@ -127,7 +135,7 @@ def findRotatedMaximalRectangle(img, nbre_angle=4, limit_image_size=300):
# angle_range = ([(90.,180.),])
angle_range = ([(0., 90.), ])
coeff1 = optimize.brute(residual, angle_range, args=(img_odd,), Ns=nbre_angle, finish=None)
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]
......@@ -135,8 +143,9 @@ def findRotatedMaximalRectangle(img, nbre_angle=4, limit_image_size=300):
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
self.scale_factor = scale_factor
......@@ -94,7 +94,7 @@ def getFTIRAperture(partImg: np.ndarray) -> FTIRAperture:
return aperture
def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None):
def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None, ftir: bool = False):
if fullimage is None:
fullimage = loadFullimageFromDataset(dataset)
if zimg is None:
......@@ -122,16 +122,17 @@ def getParticleStatsWithPixelScale(contour, dataset, fullimage=None, zimg=None):
partImg, extrema = getParticleImageFromFullimage(cnt, fullimage)
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
if ftir:
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
......
......@@ -25,6 +25,8 @@ from PyQt5 import QtWidgets, QtCore
from .particlePainter import ParticlePainter
from . import particleCharacterization as pc
from ..errors import NotConnectedContoursError
from ..helperfunctions import hasFTIRControl
class ParticleContextMenu(QtWidgets.QMenu):
combineParticlesSignal = QtCore.pyqtSignal(list, str)
......@@ -132,11 +134,12 @@ class ParticleContextMenu(QtWidgets.QMenu):
class ParticleEditor(QtCore.QObject):
particleAssignmentChanged = QtCore.pyqtSignal()
def __init__(self, viewparent, particleContainer):
super(ParticleEditor, self).__init__()
self.particleContainer = particleContainer
self.viewparent = viewparent #the assigned analysis widget
self.backupFreq = 3 #save a backup every n actions
self.viewparent = viewparent # the assigned analysis widget
self.backupFreq = 3 # save a backup every n actions
self.neverBackedUp = True
self.actionCounter = 0
......@@ -230,7 +233,9 @@ class ParticleEditor(QtCore.QObject):
self.particlePainter = None
def mergeParticlesInParticleContainerAndSampleView(self, indices, newContour, assignment):
stats = pc.getParticleStatsWithPixelScale(newContour, self.viewparent.dataset, fullimage=self.viewparent.imgdata)
isFTIR: bool = hasFTIRControl(self.viewparent)
stats = pc.getParticleStatsWithPixelScale(newContour, self.viewparent.dataset,
fullimage=self.viewparent.imgdata, ftir=isFTIR)
self.viewparent.addParticleContourToIndex(newContour, len(self.viewparent.contourItems)-1)
self.particleContainer.addMergedParticle(indices, newContour, stats, newAssignment=assignment)
......
......@@ -26,7 +26,7 @@ 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 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
......@@ -34,9 +34,15 @@ from .errors import InvalidParticleError, showErrorMessageAsWidget
from .uielements import TimeEstimateProgressbar
from .scenePyramid import ScenePyramid
from .gepardlogging import setDefaultLoggingConfig
from .helperfunctions import hasFTIRControl
screenHeight = GetSystemMetrics(1)
Nscreen = np.clip(1000, 0, round(screenHeight*0.9)) # account for too small displays
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):
......@@ -203,6 +209,7 @@ class ImageView(QtWidgets.QLabel):
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":
......@@ -304,6 +311,7 @@ class ParticleDetectionView(QtWidgets.QWidget):
def makeShowLambda(name):
def f(): return self.detectShow(name)
return QtCore.pyqtSlot()(f)
def makeValueLambda(objmethod):
return lambda : objmethod()
......@@ -421,6 +429,7 @@ class ParticleDetectionView(QtWidgets.QWidget):
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)
......@@ -599,6 +608,7 @@ class ParticleDetectionView(QtWidgets.QWidget):
width and height of the full image
'''
width, height = self.pyramid.getBoundingRectDim()
Nscreen = getScreenHeight()
if center is None:
centerx = width//2
......@@ -820,23 +830,19 @@ class ParticleDetectionView(QtWidgets.QWidget):
self.dataset.save()
def getParticleStats(self, contours):
# processWindow = TimeEstimateProgressbar()
# processWindow.setWindowTitle('Updating Particle Stats')
# processWindow.setMaxValue(len(contours))
# processWindow.enable()
# # processWindow.show()
particlestats = []
zvalimg = loadZValImageFromDataset(self.dataset)
invalidParticleIndices = []
ftir: bool = hasFTIRControl(self.view)
for contourIndex, contour in enumerate(contours):
try:
stats = getParticleStatsWithPixelScale(contour, self.dataset, fullimage=self.img, zimg=zvalimg)
stats = getParticleStatsWithPixelScale(contour, self.dataset, fullimage=self.img, zimg=zvalimg, ftir=ftir)
except InvalidParticleError:
print('Invalid contour in detection, skipping particle.')
self.logger.debug('Invalid contour in detection, skipping particle.')
invalidParticleIndices.append(contourIndex)
continue
particlestats.append(stats)
# processWindow.setValue(contourIndex)
return particlestats, invalidParticleIndices
......
This diff is collapsed.
This diff is collapsed.
# -*- 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 QtCore, QtWidgets
import numpy as np
from multiprocessing import Process, Queue, Event
import queue
import os
import logging
import logging.handlers
from typing import TYPE_CHECKING
from ..external import tsp
from .uielements import TimeEstimateProgressbar
from ..gepardlogging import setDefaultLoggingConfig
from ..analysis.particleCharacterization import FTIRAperture
from ..helperfunctions import getRamanControl
if TYPE_CHECKING:
from ..sampleview import SampleView
def reorder(points, N=20):
"""
Finds an efficient reordering of scan points in meandering horizontal
stripes.
Parameters
----------
points : list of scan points in 2D
N : integer, optional
The number of horizontal stripes in which the points should be aranged.
The default is 20.
Returns
-------
newind : index array
The new index array to reorder the scan points for efficient travel
between points.
"""
y0, y1 = points[:,1].min(), points[:,1].max()
y = np.linspace(y0,y1+.1,N+1)
allind = np.arange(points.shape[0])
newind = []
for i, yi in enumerate(y[:-1]):
yi1 = y[i+1]
indy = allind[(points[:,1]>=yi)&(points[:,1]<yi1)]
p = points[indy,:]
indx = p[:,0].argsort()
if i%2==1:
newind.append(indy[indx])
else:
newind.append(indy[indx[::-1]])
newind = np.concatenate(newind, axis=0)
assert np.unique(newind).shape[0]==allind.shape[0]
return newind
def scan(ramanSettings, positions, controlclass, dataqueue, stopevent,
logpath=''):
if logpath != '':
logger = logging.getLogger('RamanScanLogger')
logger.addHandler(
logging.handlers.RotatingFileHandler(
logpath, maxBytes=5 * (1 << 20), backupCount=10)
)
setDefaultLoggingConfig(logger)
try:
ramanctrl = getRamanControl(controlclass, logger)
ramanctrl.connect()
if ramanctrl.name == 'ThermoFTIRCom':
ramanSettings['Apertures'] = positions
ramanctrl.initiateMeasurement(ramanSettings)
logger.info(ramanctrl.name)
for i, p in enumerate(positions):
if not ramanctrl.name == 'ThermoFTIRCom':
x, y, z = p
else:
x, y, z = p.centerX, p.centerY, p.centerZ
width, height, angle = p.width, p.height, p.angle
logger.info(f'{width}, {height}, {angle}')
ramanctrl.setAperture(width, height, angle)
logger.info(f"position: {x}, {y}, {z}")
ramanctrl.moveToAbsolutePosition(x, y, z)
logger.info("move done")
ramanctrl.triggerMeasurement(i)
logger.info("trigger done")
if stopevent.is_set():
ramanctrl.disconnect()
return
dataqueue.put(i)
ramanctrl.finishMeasurement()
ramanctrl.disconnect()
except:
logger.exception('Fatal error in ramanscan')
from .errors import showErrorMessageAsWidget
showErrorMessageAsWidget('See ramanscanlog in project directory for information')
class SpecScanUI(QtWidgets.QWidget):
imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field)
ramanscanUpdate = QtCore.pyqtSignal()
# def __init__(self, ramanctrl, dataset, logger, parent=None):
def __init__(self, sampleview: 'SampleView'):
super().__init__(sampleview, QtCore.Qt.Window)
self.view = sampleview
self.logger = sampleview.logger
self.ramanctrl = sampleview.ramanctrl
self.dataset = sampleview.dataset
self.process = None
self.processstopevent = Event()
self.dataqueue = Queue()
self.timer = QtCore.QTimer(self)
vbox = QtWidgets.QVBoxLayout()
hbox = QtWidgets.QHBoxLayout()
self.params = []
self.paramsGroup = QtWidgets.QGroupBox("Raman settings")
self.paramsLayout = QtWidgets.QFormLayout()
self.prun = QtWidgets.QPushButton("Raman scan", self)
self.prun.released.connect(self.run)
self.paramsGroup.setLayout(self.paramsLayout)
self.updateRamanParameters()
self.pexit = QtWidgets.QPushButton("Cancel", self)
self.pexit.released.connect(self.cancelScan)
self.prun.setEnabled(False)
self.progressbar = TimeEstimateProgressbar()
self.progressbar.disable()
hbox.addStretch()
hbox.addWidget(self.pexit)
vbox.addWidget(self.paramsGroup)
vbox.addLayout(hbox)
vbox.addWidget(self.progressbar)
self.setLayout(vbox)
self.setWindowTitle("Raman Scan")
self.setVisible(False)
def updateRamanParameters(self):
"""
Update the raman parameters in the layout
:return:
"""
for index in reversed(range(self.paramsLayout.count())):
widget = self.paramsLayout.itemAt(index).widget()
self.paramsLayout.removeWidget(widget)
widget.setParent(None)
self.params = []
for param in self.ramanctrl.ramanParameters:
if param.dtype == 'int':
self.params.append(QtWidgets.QSpinBox())
self.params[-1].setMinimum(param.minVal)
self.params[-1].setMaximum(param.maxVal)
self.params[-1].setValue(param.value)
if param.dtype == 'double':
self.params.append(QtWidgets.QDoubleSpinBox())
self.params[-1].setMinimum(param.minVal)
self.params[-1].setMaximum(param.maxVal)
self.params[-1].setValue(param.value)
if param.dtype == 'combobox':
self.params.append(QtWidgets.QComboBox())
self.params[-1].addItems([str(i) for i in param.valList])
if param.dtype == 'checkBox':
self.params.append(QtWidgets.QCheckBox())
self.params[-1].setChecked(param.value)
for index, param in enumerate(self.params):
param.setMinimumWidth(70)
self.paramsLayout.addRow(QtWidgets.QLabel(self.ramanctrl.ramanParameters[index].name), param)
self.paramsLayout.addRow(self.prun)
self.paramsGroup.setLayout(self.paramsLayout)
def makeGetFnameLambda(self, msg, path, fileType, btn):
return lambda: self.getFName(msg, path, fileType, btn)
def getFName(self, msg, path, filetype, btn):
fname = QtWidgets.QFileDialog.getOpenFileName(self, msg, path, filetype)[0]
btn.setText(fname.split('\\')[-1])
btn.setMinimumSize(btn.sizeHint())
def resetDataset(self, ds):
self.dataset = ds
numParticles = self.dataset.particleContainer.getNumberOfParticles()
numMeasurements = self.dataset.particleContainer.getNumberOfMeasurements()
if numParticles>0:
self.prun.setEnabled(True)
self.setWindowTitle(f'{numParticles} Particles ({numMeasurements} Measurements)')
@QtCore.pyqtSlot()
def cancelScan(self):
if self.process is not None and self.process.is_alive():
reply = QtWidgets.QMessageBox.question(self, 'Stop raman scan?',
"Do you want to terminate the running scan?",
QtWidgets.QMessageBox.Yes |
QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
self.timer.stop()
self.ramanctrl.finishMeasurement(aborted=True)
self.progressbar.resetTimerAndCounter()
self.processstopevent.set()
self.process.terminate()
self.process.join()
self.dataqueue.close()
self.dataqueue.join_thread()
self.paramsGroup.setEnabled(True)
self.view.unblockUI()
self.close()
@QtCore.pyqtSlot()
def run(self):
if self.dataset.readin:
reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!',
"Coordinate systems might have changed since. Do you want to continue with saved coordinates?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No)
if reply == QtWidgets.QMessageBox.Yes:
self.dataset.readin = False
else:
return