From 11fdf4eda7add966d0ca5c8be5ffe713f3acfa50 Mon Sep 17 00:00:00 2001 From: Josef Brandt Date: Thu, 27 Aug 2020 12:22:56 +0200 Subject: [PATCH] new simulated raman --- __main__.py | 13 +- ramancom/ramancontrol.py | 2 +- ramancom/simulated/imageGenerator.py | 270 +++++++++++++++++++ ramancom/simulated/simulatedStage.py | 300 +++++++++++++++++++++ ramancom/{ => simulated}/simulatedraman.py | 246 ++++++++--------- 5 files changed, 699 insertions(+), 132 deletions(-) create mode 100644 ramancom/simulated/imageGenerator.py create mode 100644 ramancom/simulated/simulatedStage.py rename ramancom/{ => simulated}/simulatedraman.py (59%) diff --git a/__main__.py b/__main__.py index 003cae6..5fdffa8 100644 --- a/__main__.py +++ b/__main__.py @@ -72,6 +72,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): def closeEvent(self, event): self.view.closeEvent(event) + closeAll() @QtCore.pyqtSlot(float) def scalingChanged(self, scale): @@ -336,7 +337,17 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): if __name__ == '__main__': import sys from time import localtime, strftime - + + def closeAll() -> None: + """ + Closes the app and, with that, all windows. + Josef: I implemented this, as with the simulated microscope stage it was difficult to find a proper way to + ONLY close it at the end of running the program. Closing it on disconnect of the ramanctrl is not suitable, + as it should be opened also in disconnected stage (e.g., when another instance is running in optical or raman + scan, but the UI (disconnected) should still update what's going on. + """ + app.deleteLater() + def excepthook(excType, excValue, tracebackobj): """ Global function to catch unhandled exceptions. diff --git a/ramancom/ramancontrol.py b/ramancom/ramancontrol.py index 6a74e72..679750a 100644 --- a/ramancom/ramancontrol.py +++ b/ramancom/ramancontrol.py @@ -43,7 +43,7 @@ except KeyError: pass if interface == "SIMULATED_RAMAN_CONTROL": - from .simulatedraman import SimulatedRaman + from .simulated.simulatedraman import SimulatedRaman RamanControl = SimulatedRaman print("WARNING: using only simulated raman control!") simulatedRaman = True diff --git a/ramancom/simulated/imageGenerator.py b/ramancom/simulated/imageGenerator.py new file mode 100644 index 0000000..896c5da --- /dev/null +++ b/ramancom/simulated/imageGenerator.py @@ -0,0 +1,270 @@ +# -*- 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 . + +Image Generator for fake images for testing with simulated instrument interfaces +""" + + +import cv2 +import numpy as np +import matplotlib.pyplot as plt +import time +import noise +from copy import deepcopy +from typing import Tuple, List + + +def particleIsVisible(particle: np.ndarray, full: np.ndarray, pos: Tuple[int, int]) -> bool: + """ + :param full: image of the current particle + :param particle: image with all particles + :param pos: x, y of the particle center in pixels + """ + isVisible: bool = False + height, width = particle.shape[:2] + if pos[0] + width/2 >= 0 and pos[0] - width/2 < full.shape[1]: + if pos[1] + height/2 >= 0 and pos[1] - height/2 < full.shape[0]: + isVisible = True + return isVisible + + +def addTexture(partImage: np.ndarray) -> np.ndarray: + shape = partImage.shape + startX = np.random.randint(0, 100) + startY = np.random.randint(0, 100) + for i in range(shape[0]): + for j in range(shape[1]): + noiseVal: float = noise.pnoise2((startY + i)/25, (startX + j)/25, octaves=4, persistence=0.5) + noiseVal = np.clip(noiseVal + 0.5, 0.0, 1.0) # bring to 0...1 range + partImage[i, j, :] *= (noiseVal**0.3) + return partImage + + +class OverlapRange: + fullStartX: int = np.nan + fullEndX: int = np.nan + fullStartY: int = np.nan + fullEndY: int = np.nan + + subStartX: int = np.nan + subEndX: int = np.nan + subStartY: int = np.nan + subEndY: int = np.nan + + +def getParticleOverlapRanges(full: np.ndarray, sub: np.ndarray, centerXY: Tuple[int, int]) -> Tuple[bool, OverlapRange]: + ranges: OverlapRange = OverlapRange() + ranges.fullStartX = int(round(centerXY[0] - sub.shape[1]/2)) + ranges.fullEndX = ranges.fullStartX + sub.shape[1] + ranges.fullStartY = int(round(centerXY[1] - sub.shape[0]/2)) + ranges.fullEndY = ranges.fullStartY + sub.shape[0] + + origRanges = deepcopy(ranges) + + if ranges.fullEndX < 0 or ranges.fullEndY < 0 or ranges.fullStartX >= full.shape[1] or ranges.fullStartY >= full.shape[0]: + success = False + else: + success = True + if origRanges.fullStartX >= 0: + ranges.subStartX = 0 + else: + ranges.subStartX = -origRanges.fullStartX + ranges.fullStartX = 0 + + if origRanges.fullEndX <= full.shape[1]: + ranges.subEndX = sub.shape[1] + else: + diff: int = origRanges.fullEndX - full.shape[1] + ranges.subEndX = sub.shape[1] - diff + ranges.fullEndX = full.shape[1] + + if origRanges.fullStartY >= 0: + ranges.subStartY = 0 + else: + ranges.subStartY = -origRanges.fullStartY + ranges.fullStartY = 0 + + if origRanges.fullEndY <= full.shape[0]: + ranges.subEndY = sub.shape[0] + else: + diff: int = origRanges.fullEndY - full.shape[0] + ranges.subEndY = sub.shape[0] - diff + ranges.fullEndY = full.shape[0] + + return success, ranges + + +class FakeParticle: + """ A fake spherical particle""" + x: float = np.nan # in µm + y: float = np.nan # in µm + radius: float = np.nan # in µm + randImageIndex: int = np.nan + + +class FakeCamera: + # TODO: Implement a small angle to simulate a tilted camera! + def __init__(self): + self.imgDims: Tuple[int, int] = (500, 250) # width, height of camera imgage + self.pixelscale: float = 1.0 # µm/px + self.sizeScale: float = 100.0 # smaller values make particles rendered smaller and vice versa + self.threshold: float = 0.7 # threshold for determining particles. Larger values -> smaller particles + self.numZLevels: int = 7 # number of z-levels cached for faking depth of field + self.maxZDiff: float = 100 # max difference in z that is simulated. + self.currentImage: np.ndarray = np.zeros((self.imgDims[1], self.imgDims[0], 3)) + self.fakeFilter: FakeFilter = FakeFilter() + + def updateImageAtPosition(self, position: List[float]) -> None: + """ + Centers the camera at the given µm coordinates and returns the camera image centered at this position. + :param position: tuple (x in µm, y in µm, z in µm) + """ + particles: np.ndarray = np.zeros((self.imgDims[1], self.imgDims[0], 3), dtype=np.uint8) + for particle in self.fakeFilter.particles: + pixelX, pixelY = self._getParticlePixelCoords((position[0], position[1]), particle) + blurRadius: int = int(round(abs(position[2] - particle.radius) / 2)) # we just use radius as height.. + if blurRadius % 2 == 0: + blurRadius += 1 + particleImg: np.ndarray = self.fakeFilter.getParticleImage(particle, blurRadius=blurRadius) + particles = self._addParticleImg(particles, particleImg, (pixelX, pixelY)) + + for i in range(3): + particles[:, :, i] = np.flipud(particles[:, :, i]) + self.currentImage = particles + + def _addParticleImg(self, allParts: np.ndarray, curPart: np.ndarray, position: Tuple[int, int]) -> np.ndarray: + """ + Adds the current particle image to the image with all particles. + :param allParts: image with all particles + :param curPart: image of the current particle + :param position: x, y of the particle center + """ + def overlayImages(img1: np.ndarray, img2: np.ndarray) -> np.ndarray: + assert img1.shape == img2.shape + merge = np.zeros_like(img1) + mask = np.sum(img1, axis=2) > 128 + merge[mask] = img1[mask] + merge[~mask] = img2[~mask] + return np.uint8(merge) + + particleFits, ranges = getParticleOverlapRanges(allParts, curPart, position) + if particleFits: + img1 = allParts[ranges.fullStartY:ranges.fullEndY, ranges.fullStartX:ranges.fullEndX, :] + img2 = curPart[ranges.subStartY:ranges.subEndY, ranges.subStartX:ranges.subEndX, :] + assert img1.shape == img2.shape + blend = overlayImages(img1, img2) + allParts[ranges.fullStartY:ranges.fullEndY, ranges.fullStartX:ranges.fullEndX, :] = blend + + return allParts + + def _getParticlePixelCoords(self, position: Tuple[float, float], particle: FakeParticle) -> Tuple[int, int]: + """Converts the absolute particle x,y coordinates into image pixel coordinates + :param position: x and y coordinates of stage center (in µm) + :param particle: the particle in consideration + :return (x, y): pixel Coordinates of x and y of particle in image + """ + imgHeightMicrons, imgWidthMicrons = self.imgDims[1] * self.pixelscale, self.imgDims[0] * self.pixelscale + upperLeftX, upperLeftY = position[0] - imgWidthMicrons/2, position[1] - imgHeightMicrons/2 + particlePixelX: int = int(round((particle.x - upperLeftX) / self.pixelscale)) + particlePixelY: int = int(round((particle.y - upperLeftY) / self.pixelscale)) + return particlePixelX, particlePixelY + + +class FakeFilter: + def __init__(self): + self.numParticles: int = 500 + self.xRange: Tuple[float, float] = (-3000, 3000) # min and max of x Dimensions (in µm) + self.yRange: Tuple[float, float] = (-3000, 3000) # min and max of x Dimensions (in µm) + self.particleSizeRange: Tuple[float, float] = (5, 50) # min and max of particle radius (in µm) + self.numIndParticles: int = 10 # number of individual particles presets to generate + self.numBlurSteps: int = 7 # number of blur steps to simulate (additionally to image without any blur) + self.maxBlurSize: int = 51 # highest blur radius + self.baseImageSize: int = 100 + self.presetParticles: List[dict] = self._generagePresetParticles() + self.particles: List[FakeParticle] = [] + self._generateParticles() + + def getParticleImage(self, particle: FakeParticle, blurRadius: int = 0) -> np.ndarray: + def getSizeCorrectedBlurRadius(blurRad: int, scaling: float) -> int: + """If the images are scaled down later, we want to pick a stronger blurred image at this point""" + newRad: int = int(round(blurRad / scaling)) + if newRad > 0 and newRad % 2 == 0: + newRad += 1 + return newRad + + blurredImages: dict = self.presetParticles[particle.randImageIndex] + scaleFac: float = particle.radius / (self.baseImageSize / 2) + blurRadius = getSizeCorrectedBlurRadius(blurRadius, scaleFac) + + if blurRadius == 0: + img: np.ndarray = blurredImages[0] + else: + availableRadii: np.ndarray = np.array([i for i in blurredImages.keys()]) + closestRadius = availableRadii[np.argmin(abs(availableRadii-blurRadius))] + img: np.ndarray = blurredImages[closestRadius] + + return cv2.resize(img, None, fx=scaleFac, fy=scaleFac) + + def _generagePresetParticles(self) -> List[dict]: + """ + The list contains a dict for each particle variation, containing the image in different blur stages. + The key of that dicts is the used blur radius + """ + # TODO: Consider moving that into a separate thread.. it takes quite some seconds at startup.. + def getBlurLevels() -> np.ndarray: + levels: np.ndarray = np.linspace(1, self.maxBlurSize, self.numBlurSteps) + for j in range(len(levels)): + levels[j] = int(round(levels[j])) + if levels[j] % 2 == 0: + levels[j] += 1 + return levels + + baseSize = self.baseImageSize + presetParticles: List[dict] = [] + np.random.seed(42) + for i in range(self.numIndParticles): + baseImg: np.ndarray = np.zeros((baseSize + self.maxBlurSize, baseSize + self.maxBlurSize, 3)) + centerX = centerY = int(round(baseImg.shape[0] / 2)) + radius: int = int(round(baseSize/2)) + rgb: np.ndarray = 0.7 + np.random.rand(3) * 0.3 # desaturate a bit.. + randColor = (int(rgb[0] * 255), int(rgb[1] * 255), int(rgb[2] * 255)) + cv2.circle(baseImg, (centerX, centerY), radius, randColor, thickness=-1) + baseImg = np.uint8(addTexture(baseImg)) + + newParticleDict: dict = {0: baseImg} + for radius in getBlurLevels(): + radius = int(radius) + newParticleDict[radius] = cv2.GaussianBlur(baseImg, (radius, radius), sigmaX=0) + + presetParticles.append(newParticleDict) + return presetParticles + + def _generateParticles(self) -> None: + self.particles = [] + np.random.seed(42) + for i in range(self.numParticles): + rands: np.ndarray = np.random.rand(3) + newParticle: FakeParticle = FakeParticle() + newParticle.x = self.xRange[0] + rands[0]*(self.xRange[1]-self.xRange[0]) + newParticle.y = self.yRange[0] + rands[1] * (self.yRange[1] - self.yRange[0]) + newParticle.radius = self.particleSizeRange[0] + rands[2] * ( + self.particleSizeRange[1] - self.particleSizeRange[0]) + newParticle.randImageIndex = np.random.randint(0, self.numIndParticles) + self.particles.append(newParticle) diff --git a/ramancom/simulated/simulatedStage.py b/ramancom/simulated/simulatedStage.py new file mode 100644 index 0000000..2c86e94 --- /dev/null +++ b/ramancom/simulated/simulatedStage.py @@ -0,0 +1,300 @@ +# -*- 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 . + +Simulates a Microscope Stage with Camera. +""" +import os +import sys +from PyQt5 import QtWidgets, QtGui, QtCore +import cv2 +import json +import numpy as np +from typing import List +try: + from .imageGenerator import FakeCamera +except ImportError: + from imageGenerator import FakeCamera + + +class SimulatedStage(object): + def __init__(self, ui: bool = True): + super(SimulatedStage, self).__init__() + self.currentpos: List[float] = [0.0, 0.0, 0.0] # µm pos in x, y and z + self.offset: List[float] = [0.0, 0.0, 0.0] # µm offset in x, y and z + self.rotMatrix: np.ndarray = np.zeros((3, 3)) + self.camera: FakeCamera = FakeCamera() + self.camera.updateImageAtPosition(self.currentpos) + self.filepath: str = self._getFilePath() + self.uiEnabled = ui + if self.uiEnabled: + self.ui: SimulatedStageUI = SimulatedStageUI(self) + self.ui.show() + + def getCurrentCameraImage(self) -> np.ndarray: + return self.camera.currentImage + + def moveToPosition(self, x: float, y: float, z: float): + self.currentpos = [x, y, z] + self.saveConfigToFile() + self.camera.updateImageAtPosition(self.currentpos) + if self.uiEnabled: + self.ui.updateCameraImage() + self.ui.updateStageCoords() + + def connect(self) -> None: + self.updateConfigFromFile() + if self.uiEnabled: + self.ui.setEnabled(True) + + def disconnect(self) -> None: + self.saveConfigToFile() + if self.uiEnabled: + self.ui.setEnabled(False) + + def updateConfigFromFile(self) -> None: + if os.path.exists(self.filepath): + with open(self.filepath, 'r') as fp: + try: + configHasChanged: bool = self._dictToConfig(json.load(fp)) + except json.JSONDecodeError: + # TODO: get reference to logger and log info that loading config failed.. + print('failed updating simulated stage state from file..') + return + + if configHasChanged: + self.camera.updateImageAtPosition(self.currentpos) + if self.uiEnabled: + self.ui.updateCameraImage() + self.ui.updateStageCoords() + + def saveConfigToFile(self) -> None: + if self.filepath != '': + with open(self.filepath, 'w') as fp: + json.dump(self._configToDict(), fp) + + def _getFilePath(self) -> str: + self.app: QtWidgets.QApplication = QtWidgets.QApplication(sys.argv) # has to be an instance attribute :/ + self.app.setApplicationName("GEPARD") + path: str = QtCore.QStandardPaths.writableLocation(QtCore.QStandardPaths.AppLocalDataLocation) + return os.path.join(path, 'simulatedStageConfig.txt') + + def _configToDict(self) -> dict: + return {'currentPos': self.currentpos} + + def _dictToConfig(self, inputDict: dict) -> bool: + configChanged: bool = False + if self.currentpos != inputDict['currentPos']: + self.currentpos = inputDict['currentPos'] + configChanged = True + return configChanged + + +class SimulatedStageUI(QtWidgets.QWidget): + def __init__(self, stageParent: SimulatedStage): + super(SimulatedStageUI, self).__init__() + self.stageParent: SimulatedStage = stageParent + self.updateTimer: QtCore.QTimer = QtCore.QTimer() + self.updateTimer.setSingleShot(False) + self.updateTimer.timeout.connect(self.stageParent.updateConfigFromFile) + self.updateTimer.start(500) + + self.setWindowTitle('Simulated Microscope Stage') + coordsGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Stage Coordinates') + coordsLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() + coordsGroup.setLayout(coordsLayout) + self.labelX: QtWidgets.QLabel = QtWidgets.QLabel() + self.labelY: QtWidgets.QLabel = QtWidgets.QLabel() + self.labelZ: QtWidgets.QLabel = QtWidgets.QLabel() + for label in [self.labelX, self.labelY, self.labelZ]: + coordsLayout.addWidget(label) + + self.camView: QtWidgets.QGraphicsView = self._createCamView() + + self.stepSizeSpinbox: QtWidgets.QSpinBox = QtWidgets.QSpinBox() + self.stageControls: QtWidgets.QGroupBox = self._createControls() + + layout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() + self.setLayout(layout) + + leftColumn: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() + leftColumn.addWidget(coordsGroup) + leftColumn.addWidget(QtWidgets.QLabel('Camera Image')) + leftColumn.addWidget(self.camView) + leftColumn.addWidget(self.stageControls) + + layout.addLayout(leftColumn) + + rightColumn: QtWidgets.QGroupBox = self._getExtrasColumn() + layout.addWidget(rightColumn) + self.updateCameraImage() + self.updateStageCoords() + + def _createCamView(self) -> QtWidgets.QGraphicsView: + camView: QtWidgets.QGraphicsView = QtWidgets.QGraphicsView() + camView.item = QtWidgets.QGraphicsPixmapItem() + scene = QtWidgets.QGraphicsScene(camView) + scene.addItem(camView.item) + camView.setScene(scene) + camView.scale(1.0, 1.0) + return camView + + def _createControls(self) -> QtWidgets.QGroupBox: + def makeBtnLambda(_btn: QtWidgets.QPushButton): + return lambda: self._moveBtnPressed(_btn) + + controls: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Stage Controls') + layout: QtWidgets.QGridLayout = QtWidgets.QGridLayout() + controls.setLayout(layout) + + stepSizeLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() + stepSizeLayout.addStretch() + stepSizeLayout.addWidget(QtWidgets.QLabel('Set Step Size (µm): ')) + stepSizeLayout.addWidget(self.stepSizeSpinbox) + stepSizeLayout.addStretch() + + self.stepSizeSpinbox.setMinimum(0) + self.stepSizeSpinbox.setValue(10) + self.stepSizeSpinbox.setMaximum(100000) + self.stepSizeSpinbox.setSingleStep(50) + self.stepSizeSpinbox.setMaximumWidth(75) + + xyGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('XY Controls') + xyLayout: QtWidgets.QGridLayout = QtWidgets.QGridLayout() + xyGroup.setLayout(xyLayout) + upBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('UP') + downBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('DOWN') + leftBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('LEFT') + rightBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('RIGHT') + for btn in [upBtn, downBtn, leftBtn, rightBtn]: + btn.clicked.connect(makeBtnLambda(btn)) + + xyLayout.addWidget(upBtn, 0, 1) + xyLayout.addWidget(leftBtn, 1, 0) + xyLayout.addWidget(rightBtn, 1, 2) + xyLayout.addWidget(downBtn, 2, 1) + + zGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Z Controls') + zLayout: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() + zGroup.setLayout(zLayout) + + zUpBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('HIGHER') + zDownBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('LOWER') + for btn in [zUpBtn, zDownBtn]: + btn.clicked.connect(makeBtnLambda(btn)) + zLayout.addWidget(btn) + + layout.addLayout(stepSizeLayout, 0, 0, 1, 2) + layout.addWidget(xyGroup, 1, 0) + layout.addWidget(zGroup, 1, 1) + return controls + + def _getExtrasColumn(self) -> QtWidgets.QGroupBox: + def makePresetBtnLambda(pressedBtn: QtWidgets.QPushButton): + return lambda: self._moveToPresetPosition(pressedBtn) + + extrasGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Extra Operations') + extrasLayout: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() + extrasGroup.setLayout(extrasLayout) + + presetPositionsGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Move to preset positions') + presetPositionsLayout: QtWidgets.QGridLayout = QtWidgets.QGridLayout() + presetPositionsGroup.setLayout(presetPositionsLayout) + + upperLeftBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('UpperLeft') + lowerLeftBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('LowerLeft') + upperRightBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('UpperRight') + lowerRightBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('LowerRight') + centerBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Center') + for btn in [upperLeftBtn, upperRightBtn, lowerLeftBtn, lowerRightBtn, centerBtn]: + btn.clicked.connect(makePresetBtnLambda(btn)) + + presetPositionsLayout.addWidget(upperLeftBtn, 0, 0) + presetPositionsLayout.addWidget(upperRightBtn, 0, 2) + presetPositionsLayout.addWidget(centerBtn, 1, 1) + presetPositionsLayout.addWidget(lowerLeftBtn, 2, 0) + presetPositionsLayout.addWidget(lowerRightBtn, 2, 2) + + coordGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Modify Coordinate System') + coordLayout: QtWidgets.QFormLayout = QtWidgets.QFormLayout() + coordGroup.setLayout(coordLayout) + + extrasLayout.addWidget(presetPositionsGroup) + extrasLayout.addWidget(coordGroup) + return extrasGroup + + def _moveBtnPressed(self, btn: QtWidgets.QPushButton) -> None: + label: str = btn.text() + newPos: List[float] = self.stageParent.currentpos + if label == 'UP': + newPos[1] += self.stepSizeSpinbox.value() + elif label == 'DOWN': + newPos[1] -= self.stepSizeSpinbox.value() + elif label == 'LEFT': + newPos[0] -= self.stepSizeSpinbox.value() + elif label == 'RIGHT': + newPos[0] += self.stepSizeSpinbox.value() + elif label == 'HIGHER': + newPos[2] += self.stepSizeSpinbox.value() + elif label == 'LOWER': + newPos[2] -= self.stepSizeSpinbox.value() + + self.stageParent.moveToPosition(newPos[0], newPos[1], newPos[2]) + + def _moveToPresetPosition(self, btn: QtWidgets.QPushButton, stepSize: float = 1000) -> None: + label: str = btn.text() + newPos: List[float] = [0.0, 0.0, 0.0] + if label == 'UpperLeft': + newPos[0] -= stepSize + newPos[1] += stepSize + elif label == 'UpperRight': + newPos[0] += stepSize + newPos[1] += stepSize + elif label == 'LowerLeft': + newPos[0] -= stepSize + newPos[1] -= stepSize + elif label == 'LowerRight': + newPos[0] += stepSize + newPos[1] -= stepSize + elif label == 'Center': + pass + + self.stageParent.moveToPosition(newPos[0], newPos[1], newPos[2]) + + def updateCameraImage(self) -> None: + img: np.ndarray = self.stageParent.camera.currentImage.copy() + height, width, channel = img.shape + centerX, centerY = int(round(width/2)), int(round(height/2)) + cv2.circle(img, (centerX, centerY), 20, (0, 255, 0), thickness=2) + bytesPerLine = 3 * width + pixmap = QtGui.QPixmap() + pixmap.convertFromImage(QtGui.QImage(img.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) + self.camView.item.setPixmap(pixmap) + + def updateStageCoords(self) -> None: + self.labelX.setText(f'x: {round(self.stageParent.currentpos[0], 2)} µm') + self.labelY.setText(f'y: {round(self.stageParent.currentpos[1], 2)} µm') + self.labelZ.setText(f'z: {round(self.stageParent.currentpos[2], 2)} µm') + + +if __name__ == '__main__': + import sys + app = QtWidgets.QApplication(sys.argv) + stage: SimulatedStage = SimulatedStage(ui=True) + app.exec_() diff --git a/ramancom/simulatedraman.py b/ramancom/simulated/simulatedraman.py similarity index 59% rename from ramancom/simulatedraman.py rename to ramancom/simulated/simulatedraman.py index 9c22720..5acf4ce 100644 --- a/ramancom/simulatedraman.py +++ b/ramancom/simulated/simulatedraman.py @@ -1,130 +1,116 @@ -# -*- 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 . - -Simualted Raman interface module for testing without actual raman system connected -""" - -import sys, os -stdout = sys.stdout -from time import sleep -import numpy as np -from shutil import copyfile -from .ramanbase import RamanBase - -class SimulatedRaman(RamanBase): - magn = 20 - ramanParameters = {} - def __init__(self, logger): - super().__init__() - self.name = 'SimulatedRaman' - self.logger = logger - self.currentpos = None, 0., 0. - self.currentZ = 0. - # some plausible data to simulate consecutively changing positions - self.positionlist = np.array([[ -1201, 1376, -1290], - [ -1195, -1200, -1279], - [ 1097, -1254, -1297], - [ 2704.1, 1288.2, -1381], - [ 1884. , -1500.8, -1381]]) - self.znum = 4 - self.gridnum = 36 - self.positionindex = 0 - self.imageindex = 0 - - def getRamanPositionShift(self): - return 0., 0. - - def connect(self): - self.connected = True - self.imageindex = 0 - return True - - def disconnect(self): - self.connected = False - - def getPosition(self): - if self.currentpos[0] is None: - pos = self.positionlist[self.positionindex] - self.positionindex = (self.positionindex+1)%len(self.positionlist) - else: - pos = self.currentpos - return pos - - def getSoftwareZ(self): - return self.currentpos[2] - - def getUserZ(self): - assert self.connected - if self.currentpos[0] is None: - index = (self.positionindex-1)%len(self.positionlist) - return self.positionlist[index][2] - else: - return self.currentZ - - def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): - assert self.connected - self.logger.info(f'moving to: x: {x}, y: {y}, z: {z}') - if z is None: - self.currentpos = x, y, self.currentpos[2] - else: - self.currentpos = x, y, z - sleep(0.1) - - def moveZto(self, z, epsz=0.011): - assert self.connected - self.currentpos = self.currentpos[0], self.currentpos[1], z - - def saveImage(self, fname): - assert self.connected - cwd = os.getcwd() - fakeImgPath = os.path.join(cwd, "gepard", "fakeData/image.bmp") - copyfile(fakeImgPath, fname) - self.imageindex = (self.imageindex+1)%(self.znum*self.gridnum) - sleep(.01) - - def getImageDimensions(self, mode = 'df'): - """ - Get the image width and height in um and the orientation angle in degrees. - """ - assert self.connected - width, height, angle = 463.78607177734375, 296.0336608886719, -0.04330849274992943 - return width, height, angle - - def startSinglePointScan(self): - assert self.connected - self.logger.info("Fake scan") - sleep(.3) - - def initiateMeasurement(self, ramanSettings): - assert self.connected - self.logger.info(f"Scanning {ramanSettings['numPoints']} particle positions") - self.timeseries = ramanSettings['numPoints'] - sleep(.1) - - - def triggerMeasurement(self, num): - assert self.timeseries - self.logger.info(f"Scan number: {num}") - sleep(.1) - if num==self.timeseries-1: - self.timeseries = False - - def finishMeasurement(self, aborted=False): - self.logger.info('measurement was aborted') \ No newline at end of file +# -*- 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 . + +Simualted Raman interface module for testing without actual raman system connected +""" + +import sys +stdout = sys.stdout +from time import sleep +from ..ramanbase import RamanBase +from ...helperfunctions import cv2imwrite_fix +from .simulatedStage import SimulatedStage + + +class SimulatedRaman(RamanBase): + magn = 20 + ramanParameters = {} + simulatedInterface = True + + def __init__(self, logger, ui: bool = True): + super().__init__() + self.name = 'SimulatedRaman' + self.logger = logger + self.userZ = 0.0 + self.stage: SimulatedStage = SimulatedStage(ui=ui) + self.znum = 4 + self.gridnum = 36 + self.positionindex = 0 + self.imageindex = 0 + + def getRamanPositionShift(self): + return 0., 0. + + def connect(self): + self.stage.connect() + self.connected = True + self.imageindex = 0 + return True + + def disconnect(self): + self.stage.disconnect() + self.connected = False + + def getPosition(self): + return self.stage.currentpos + + def getSoftwareZ(self): + return self.stage.currentpos[2] + + def getUserZ(self): + assert self.connected + return self.userZ + + def moveToAbsolutePosition(self, x, y, z=None, epsxy=0.11, epsz=0.011): + assert self.connected + self.logger.info(f'moving to: x: {x}, y: {y}, z: {z}') + if z is None: + self.stage.moveToPosition(x, y, self.stage.currentpos[2]) + else: + self.stage.moveToPosition(x, y, z) + sleep(0.1) + + def moveZto(self, z, epsz=0.011): + assert self.connected + self.stage.moveToPosition(self.stage.currentpos[0], self.stage.currentpos[1], z) + + def saveImage(self, fname): + assert self.connected + cv2imwrite_fix(fname, self.stage.getCurrentCameraImage()) + self.imageindex = (self.imageindex+1) % (self.znum*self.gridnum) + + def getImageDimensions(self, mode='df'): + """ + Get the image width and height in um and the orientation angle in degrees. + """ + assert self.connected + camDims: tuple = self.stage.camera.imgDims + return camDims[0], camDims[1], 0 # TODO: RE-Implement a small angle to simulate a tilted camera! + + def startSinglePointScan(self): + assert self.connected + self.logger.info("Fake scan") + sleep(.3) + + def initiateMeasurement(self, ramanSettings): + assert self.connected + self.logger.info(f"Scanning {ramanSettings['numPoints']} particle positions") + self.timeseries = ramanSettings['numPoints'] + sleep(.1) + + def triggerMeasurement(self, num): + assert self.timeseries + self.logger.info(f"Scan number: {num}") + sleep(1.0) + if num == self.timeseries-1: + self.timeseries = False + + def finishMeasurement(self, aborted=False): + self.logger.info('measurement was aborted') -- GitLab