diff --git a/__main__.py b/__main__.py index 003cae6f4ff9c40d83b77f4e70cf1cadf54051f5..1718cbf35ef93c115d575d39424e4235d51fd6fc 100644 --- a/__main__.py +++ b/__main__.py @@ -203,6 +203,10 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): self.configRamanCtrlAct.triggered.connect(self.view.configureRamanControl) if self.view.simulatedRaman: self.configRamanCtrlAct.setDisabled(True) + + self.recalculateCoordAct = QtWidgets.QAction("&Recalculate Coordinate System") + self.recalculateCoordAct.setDisabled(True) + self.recalculateCoordAct.triggered.connect(self.view.recalculateCoordinateSystem) self.noOverlayAct = QtWidgets.QAction("&No Overlay", self) self.noOverlayAct.setShortcut("1") @@ -223,18 +227,18 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): def updateModes(self, active=None, maxenabled=None): ose, osc, pde, pdc, rse, rsc = [False]*6 - if maxenabled=="OpticalScan": + if maxenabled == "OpticalScan": ose = True - elif maxenabled=="ParticleDetection": + elif maxenabled == "ParticleDetection": ose, pde = True, True - elif maxenabled=="RamanScan": + elif maxenabled == "RamanScan": ose, pde, rse = True, True, True - if active=="OpticalScan" and ose: + if active == "OpticalScan" and ose: osc = True - elif active=="ParticleDetection" and pde: + elif active == "ParticleDetection" and pde: pdc = True - elif active=="RamanScan" and rse: + elif active == "RamanScan" and rse: rsc = True self.opticalScanAct.setEnabled(ose) @@ -292,6 +296,7 @@ class GEPARDMainWindow(QtWidgets.QMainWindow): self.toolsMenu = QtWidgets.QMenu("&Tools") self.toolsMenu.addAction(self.snapshotAct) self.toolsMenu.addAction(self.configRamanCtrlAct) + self.toolsMenu.addAction(self.recalculateCoordAct) self.dispMenu = QtWidgets.QMenu("&Display", self) self.overlayActGroup = QtWidgets.QActionGroup(self.dispMenu) diff --git a/coordinatetransform.py b/coordinatetransform.py new file mode 100644 index 0000000000000000000000000000000000000000..ec5320045195c1562014577630ac5c0ff185e6f6 --- /dev/null +++ b/coordinatetransform.py @@ -0,0 +1,94 @@ +import numpy as np +from scipy.optimize import least_squares + + +def getTransform(srcPoints: np.ndarray, dstPoints: np.ndarray, axisInvert: tuple = (False, False, False), + ramanShift: tuple = (0, 0)) -> tuple: + """ + Takes N points from source and target system and uses a least_squared method to find the transformation matrix + :param srcPoints: Nx3 array of xyz-points in source coordinate system + :param dstPoints: Nx3 array of xyz-points in target coordinate system + :param axisInvert: tuple of bools to indicate axis inversion in (x, y, z) dimension + :param ramanShift: x,y tuple of pixel offset of raman laset to image center + :returns resultTuple: transformMatrix, pc - shift, zpc, residuals + """ + points = srcPoints + points[:, 0] -= ramanShift[0] + points[:, 1] -= ramanShift[1] + Parity = np.mat(np.diag([-1. if axisInvert[0] else 1., + -1. if axisInvert[1] else 1., + -1. if axisInvert[2] else 1.])) + # zpoints = np.array([m.getPos() for m in self.markers], dtype=np.double) + zpoints = dstPoints + pc = points.mean(axis=0) + zpc = zpoints.mean(axis=0) + + points -= pc[np.newaxis, :] + zpoints -= zpc[np.newaxis, :] + + def getRotMat(angles): + c1, s1 = np.cos(angles[0]), np.sin(angles[0]) + c2, s2 = np.cos(angles[1]), np.sin(angles[1]) + c3, s3 = np.cos(angles[2]), np.sin(angles[2]) + return np.mat([[c1 * c3 - s1 * c2 * s3, -c1 * s3 - s1 * c2 * c3, s1 * s2], + [s1 * c3 + c1 * c2 * s3, -s1 * s3 + c1 * c2 * c3, -c1 * s2], + [s1 * s3, s2 * c3, c2]]) + + # find the transformation matrix with best fit for small angles in + # [-45°,45°] for all permutation of markers + # permbest = None + # pointsbest = None + ppoints = points[:, :].copy() + + def err(angles_shift): + T = (getRotMat(angles_shift[:3]).T * Parity).A + return (np.dot(zpoints, T) - angles_shift[np.newaxis, 3:] - ppoints).ravel() + + angle = np.zeros(3) + opt = least_squares(err, np.concatenate((angle, np.zeros(3))), + bounds=(np.array([-np.pi / 4] * 3 + [-np.inf] * 3), + np.array([np.pi / 4] * 3 + [np.inf] * 3)), + method='dogbox') + permbest = opt + pointsbest = ppoints + + optangles = permbest.x[:3] + shift = permbest.x[3:] + T = (getRotMat(optangles).T * Parity).A + error = (np.dot(zpoints, T) - shift[np.newaxis, :] - pointsbest) + # print("Transformation angles:", optangles, flush=True) + # print("Transformation shift:", shift, flush=True) + # print("Transformation err:", error, flush=True) + residuals = np.linalg.norm(error, axis=1) + # accept = True + if np.any(residuals > 1.): + print(f'Transformation residuals are large:{residuals}') + # ret = QtWidgets.QMessageBox.warning(self, 'Warning!', + # f'Transformation residuals are large:{residuals}', + # QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, + # QtWidgets.QMessageBox.Ok) + # if ret == QtWidgets.QMessageBox.Cancel: + # accept = False + return T, pc - shift, zpc, residuals + + +class TrayMarker: + """ + Coordinate Marker on the sample tray. Is NOT in the actual optical image. + """ + index: int = -1 + worldCoordX: float = np.nan + worldCoordY: float = np.nan + worldCoordZ: float = np.nan + + +class ImageMarker: + """ + Coordinate Marker of a characteristic feature within the optical image. + """ + index: int = -1 + worldCoordX: float = np.nan + worldCoordY: float = np.nan + worldCoordZ: float = np.nan + imgCoordX: int = -1 + imgCoordY: int = -1 diff --git a/dataset.py b/dataset.py index 184679479cd7f30a81ec63817ec391889acef054..ec6f317cd8edc46895d918ab0035401f029d24aa 100644 --- a/dataset.py +++ b/dataset.py @@ -24,6 +24,7 @@ import numpy as np import sys import cv2 from copy import copy +from typing import List from .analysis.particleContainer import ParticleContainer from .legacyConvert import legacyConversion, currentVersion from .helperfunctions import cv2imwrite_fix, cv2imread_fix @@ -31,9 +32,11 @@ from .helperfunctions import cv2imwrite_fix, cv2imread_fix # (no relative import) from . import dataset from . import analysis +from .coordinatetransform import TrayMarker, ImageMarker sys.modules['dataset'] = dataset sys.modules['analysis'] = analysis + def loadData(fname): retds = None with open(fname, "rb") as fp: @@ -120,11 +123,11 @@ class DataSet(object): self.version = currentVersion self.lastpos = None self.maxdim = None - self.pixelscale_df = None # µm / pixel --> scale of DARK FIELD camera (used for image stitching) - self.pixelscale_bf = None # µm / pixel of DARK FIELD camera (set to same as bright field, if both use the same camera) + self.pixelscale_df = None # µm / pixel --> scale of DARK FIELD camera (used for image stitching) + self.pixelscale_bf = None # µm / pixel of DARK FIELD camera (set to same as bright field, if both use the same camera) self.imagedim_bf = None # width, height, angle of BRIGHT FIELD camera self.imagedim_df = None # width, height, angle of DARK FIELD camera (set to same as bright field, if both use the same camera) - self.imagescanMode = 'df' #was the fullimage acquired in dark- or brightfield? + self.imagescanMode = 'df' # was the fullimage acquired in dark- or brightfield? self.fitpoints = [] # manually adjusted positions aquired to define the specimen geometry self.fitindices = [] # which of the five positions in the ui are already known self.boundary = [] # scan boundary computed by a circle around the fitpoints + manual adjustments @@ -132,7 +135,9 @@ class DataSet(object): self.zpositions = [] # z-positions for optical scan self.heightmap = None self.zvalimg = None - self.coordinatetransform = None # if imported form extern source coordinate system may be rotated + self.coordinatetransform = None # if imported form extern source coordinate system may be rotated + self.trayMarkers: List[TrayMarker] = [] # list of markers on the sample tray + self.imageMarkers: List[ImageMarker] = [] # list of coordinate markers within the image self.signx = 1. self.signy = -1. @@ -141,7 +146,6 @@ class DataSet(object): # parameters specifically for raman scan self.pshift = None # shift of raman scan position relative to image center - self.coordOffset = [0, 0] #offset of entire coordinate system self.seedpoints = np.array([]) self.seeddeletepoints = np.array([]) self.detectParams = {'points': np.array([[50,0],[100,200],[200,255]]), @@ -242,8 +246,6 @@ class DataSet(object): if not force: assert not self.readin p0 = copy(self.lastpos) - p0[0] += self.coordOffset[0] - p0[1] += self.coordOffset[1] if mode == 'df': p0[0] -= self.signx*self.imagedim_df[0]/2 p0[1] -= self.signy*self.imagedim_df[1]/2 @@ -262,7 +264,7 @@ class DataSet(object): if self.coordinatetransform is not None: T, pc = self.coordinatetransform - x, y, z = (np.dot(np.array([x,y,z]), T) + pc) + x, y, z = (np.dot(np.array([x, y, z]), T) + pc) if returnz: return x, y, z diff --git a/gui/coordTransferGUI.py b/gui/coordTransferGUI.py new file mode 100644 index 0000000000000000000000000000000000000000..74bf22c06e0f6314d5e28b096337a9f68281efc4 --- /dev/null +++ b/gui/coordTransferGUI.py @@ -0,0 +1,275 @@ +from PyQt5 import QtWidgets, QtGui, QtCore +import numpy as np +from typing import List +from copy import deepcopy +from ..coordinatetransform import ImageMarker, TrayMarker, getTransform +from ..ramancom.ramanbase import RamanBase + + +class CoordTransformUI(QtWidgets.QWidget): + def __init__(self, viewParent): + super(CoordTransformUI, self).__init__() + self.setWindowTitle('Recalculate Coordinate System') + self.setWindowFlags(QtCore.Qt.WindowStaysOnTopHint) + self.viewParent = viewParent + self.ramanctrl: RamanBase = self.viewParent.ramanctrl + + self.imageMarkers: List[ImageMarker] = [] # in the new coord system + self.newImageMarkers: List[ImageMarker] = [] + self.imageMarkerWidgets: List[CoordinateSystemMarker] = [] + self.trayMarkers: List[TrayMarker] = [] # in the new coord system + self.newTrayMarkers: List[TrayMarker] = [] # in the new coord system + + self.layout = QtWidgets.QVBoxLayout() + self.setLayout(self.layout) + + selectionLayout = QtWidgets.QHBoxLayout() + self.trayMarkersBtn: QtWidgets.QRadioButton = QtWidgets.QRadioButton('Use Tray Markers') + self.trayMarkersBtn.pressed.connect(self._useTrayMarkers) + self.trayMarkersBtn.setChecked(True) + self.imgMarkersBtn: QtWidgets.QRadioButton = QtWidgets.QRadioButton('Use Image Markers') + self.imgMarkersBtn.pressed.connect(self._useImageMarkers) + + selectionLayout.addWidget(self.trayMarkersBtn) + selectionLayout.addWidget(self.imgMarkersBtn) + + self.addMarkerBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Add New Markers') + self.addMarkerBtn.setStyleSheet("background-color : lightgrey") + self._formatAsToggleBtn(self.addMarkerBtn) + + self.transferResult: QtWidgets.QLabel = QtWidgets.QLabel('No valid input') + + self.trayMarkerGroup: QtWidgets.QGroupBox = self._getTrayMarkerGroup() + self.imgMarkerGroup: QtWidgets.QGroupBox = self._getImageMarkerGroup() + + saveBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Save to Dataset') + saveBtn.clicked.connect(self._saveToDataset) + + self.layout.addLayout(selectionLayout) + self.layout.addWidget(self.trayMarkerGroup) + self.layout.addWidget(self.transferResult) + self.layout.addWidget(saveBtn) + + self._getMarkersFromDataset() + + def _getMarkersFromDataset(self) -> None: + if self.viewParent is not None: + self.trayMarkers = deepcopy(self.viewParent.dataset.trayMarkers) + self.imageMarkers = deepcopy(self.viewParent.dataset.imageMarkers) + for index, imgMarker in enumerate(self.imageMarkers): + pxPos: tuple = (imgMarker.imgCoordX, imgMarker.imgCoordY) + newMarkerWidget: CoordinateSystemMarker = CoordinateSystemMarker(index, pxPos) + self.imageMarkerWidgets.append(newMarkerWidget) + self.viewParent.scene().addItem(newMarkerWidget) + + def _formatAsToggleBtn(self, btn: QtWidgets.QPushButton) -> None: + btn.setCheckable(True) + btn.setChecked(False) + btn.clicked.connect(lambda: self._setBtnColor(btn)) + + def _getTrayMarkerGroup(self) -> QtWidgets.QGroupBox: + layout = QtWidgets.QGridLayout() + group: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Sample Tray Markers') + group.setLayout(layout) + if len(self.trayMarkers) == 0: + layout.addWidget(QtWidgets.QLabel('No tray markers found.')) + for i in range(3): + layout.addWidget(QtWidgets.QLabel('New Marker Placeholder')) + return group + + def _getImageMarkerGroup(self) -> QtWidgets.QGroupBox: + def makeCenterOnLambda(ind): + return lambda: self._centerOnImageMarker(ind) + + def makeDeleteLambda(ind): + return lambda: self._deleteImageMarker(ind) + + def makeReadNewPosLambda(ind): + return lambda: self._readNewImageMarker(ind) + + layout = QtWidgets.QGridLayout() + group: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Image Based Markers') + group.setLayout(layout) + layout.addWidget(self.addMarkerBtn) + if len(self.imageMarkers) == 0: + layout.addWidget(QtWidgets.QLabel('No image markers found.')) + else: + layout.addWidget(QtWidgets.QLabel('Number'), 1, 0) + layout.addWidget(QtWidgets.QLabel('Coordinates old'), 1, 1) + layout.addWidget(QtWidgets.QLabel('Coordinates new'), 1, 2) + + for index, imgMarker in enumerate(self.imageMarkers): + # reset indices, as they could have been reordered by deleting a marker.. + imgMarker.index = index + self.imageMarkerWidgets[index].index = index + newMarker: ImageMarker = self.newImageMarkers[index] + newMarker.index = index + newX, newY, newZ = newMarker.worldCoordX, newMarker.worldCoordY, newMarker.worldCoordZ + x, y, z = imgMarker.worldCoordX, imgMarker.worldCoordY, imgMarker.worldCoordZ + row: int = 2+index + + updateBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Read Coordinates') + updateBtn.clicked.connect(makeReadNewPosLambda(index)) + focusBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Center On') + focusBtn.clicked.connect(makeCenterOnLambda(index)) + delBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Delete') + delBtn.clicked.connect(makeDeleteLambda(index)) + layout.addWidget(QtWidgets.QLabel(str(imgMarker.index+1)), row, 0) + layout.addWidget(QtWidgets.QLabel(f'x: {round(x)} µm\ny: {round(y)} µm\nz: {round(z)} µm'), row, 1) + if np.nan in [newX, newY, newZ]: + layout.addWidget(QtWidgets.QLabel('Coordinates not yet read.'), row, 2) + else: + layout.addWidget(QtWidgets.QLabel(f'x: {round(newX)} µm\ny: {round(newY)} µm\nz: {round(newZ)} µm'), row, 2) + layout.addWidget(updateBtn, row, 3) + layout.addWidget(focusBtn, row, 4) + layout.addWidget(delBtn, row, 5) + return group + + def _updateTransferResult(self) -> None: + oldMarkers: list = self.trayMarkers if self.trayMarkersBtn.isChecked() else self.imageMarkers + newMarkers: list = self.newTrayMarkers if self.trayMarkersBtn.isChecked() else self.newImageMarkers + srcPoints: np.ndarray = np.array( + [[marker.worldCoordX, marker.worldCoordY, marker.worldCoordZ] for marker in oldMarkers]) + dstPoints: np.ndarray = np.array( + [[marker.worldCoordX, marker.worldCoordY, marker.worldCoordZ] for marker in newMarkers]) + hasNan: bool = (np.any(np.isnan(srcPoints)) or np.any(np.isnan(dstPoints))) + if len(oldMarkers) == len(newMarkers) and len(oldMarkers) >= 3 and not hasNan: + transformMatrix, pc, zpc, residuals = getTransform(srcPoints, dstPoints) + self.transferResult.setText(f'Transform residuals (µm): {residuals}') + else: + self.transferResult.setText('No valid inputs for coordinate transfer') + + def _setBtnColor(self, btn: QtWidgets.QPushButton): + """ + Sets color of the toggle button to indicate its status. + """ + if btn.isChecked(): + btn.setStyleSheet("background-color : lightblue") + else: + btn.setStyleSheet("background-color : lightgrey") + + def _useTrayMarkers(self) -> None: + """ + Use markers on the sample tray that are NOT in the microscope image. + """ + self.imgMarkerGroup.setParent(None) + self.trayMarkerGroup.setParent(None) + self.trayMarkerGroup = self._getTrayMarkerGroup() + self.layout.insertWidget(1, self.trayMarkerGroup) + + def _useImageMarkers(self) -> None: + """ + Use markers that are defined through characteristic spots within the microscope image. + """ + self.imgMarkerGroup.setParent(None) + self.trayMarkerGroup.setParent(None) + self.imgMarkerGroup = self._getImageMarkerGroup() + self.layout.insertWidget(1, self.imgMarkerGroup) + self._updateTransferResult() + + def mousePressEvent(self, event: QtGui.QMouseEvent) -> None: + if self.addMarkerBtn.isChecked() and event.button() == QtCore.Qt.LeftButton: + scenePos = self.viewParent.mapToScene(event.pos()) + self._addNewImageMarker((scenePos.x(), scenePos.y())) + + def _deleteImageMarker(self, markerIndex: int) -> None: + self.imageMarkers.remove(self.imageMarkers[markerIndex]) + self.newImageMarkers.remove(self.newImageMarkers[markerIndex]) + self.viewParent.scene().removeItem(self.imageMarkerWidgets[markerIndex]) + self.imageMarkerWidgets.remove(self.imageMarkerWidgets[markerIndex]) + self._useImageMarkers() + + def _centerOnImageMarker(self, markerIndex: int) -> None: + self.viewParent.centerOn(self.imageMarkerWidgets[markerIndex]) + + def _addNewImageMarker(self, pos: tuple) -> None: + newImageMarker: ImageMarker = ImageMarker() + newImageMarker.index = len(self.imageMarkers) + x, y, z = self.ramanctrl.getPosition() + newImageMarker.worldCoordX = x + newImageMarker.worldCoordY = y + newImageMarker.worldCoordZ = z + newImageMarker.imgCoordX = pos[0] + newImageMarker.imgCoordY = pos[1] + self.imageMarkers.append(newImageMarker) + + newNewImageMarker: ImageMarker = ImageMarker() + newNewImageMarker.imgCoordX = pos[0] + newNewImageMarker.imgCoordY = pos[1] + self.newImageMarkers.append(newNewImageMarker) + + newMarkerWidget: CoordinateSystemMarker = CoordinateSystemMarker(len(self.imageMarkers), pos) + self.imageMarkerWidgets.append(newMarkerWidget) + self.viewParent.scene().addItem(newMarkerWidget) + self._useImageMarkers() + + def _readNewImageMarker(self, index: int) -> None: + """Reads current instrument coordinates and assignes them to the respective "new image marker" """ + x, y, z = self.ramanctrl.getPosition() + self.newImageMarkers[index].worldCoordX = x + self.newImageMarkers[index].worldCoordY = y + self.newImageMarkers[index].worldCoordZ = z + self._useImageMarkers() + + def _saveToDataset(self) -> None: + if self.trayMarkersBtn.isChecked(): + print('saving tray markers to dataset') + else: + print('saving image makers to dataset') + + def closeEvent(self, a0: QtGui.QCloseEvent) -> None: + reply = QtWidgets.QMessageBox.question(self, 'Close without saving', + 'The window is about to be closed.\nSave markers prior to closing?', + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.Yes) + if reply == QtWidgets.QMessageBox.Yes: + self._saveToDataset() + + if self.viewParent is not None: + self.viewParent.showHiddenWidgets() + self.viewParent.coordTransferWidget = None + del self + + +class CoordinateSystemMarker(QtWidgets.QGraphicsItem): + def __init__(self, index: int, position: tuple): + super().__init__() + self.setPos(position[0], position[1]) + self.setFlag(QtWidgets.QGraphicsItem.ItemIsMovable) + self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) + self.index = index + self.pxSize: int = 101 + self.gapSize: int = 10 + # self.setFlag(QtWidgets.QGraphicsItem.ItemSendsGeometryChanges) + # self.poschanged = False + + def boundingRect(self) -> QtCore.QRectF: + return QtCore.QRectF(-self.pxSize/2, -self.pxSize/2, self.pxSize, self.pxSize) + + def paint(self, painter: QtGui.QPainter, option, widget) -> None: + halfSize: int = int(round(self.pxSize / 2)) + halfGapSize: int = int(round(self.gapSize / 2)) + + painter.setPen(QtCore.Qt.green) + painter.setBrush(QtGui.QColor(30, 255, 30, 64)) + + painter.drawEllipse(-halfSize, -halfSize, self.pxSize, self.pxSize) + + # horizontal lines + painter.drawLine(-halfSize, 0, -halfGapSize, 0) + painter.drawLine(halfGapSize, 0, halfSize, 0) + + # vertical lines + painter.drawLine(0, -halfSize, 0, -halfGapSize) + painter.drawLine(0, halfGapSize, 0, halfSize) + + painter.drawText(-halfSize, -halfSize, str(f'Marker {self.index+1}')) + + +if __name__ == '__main__': + import sys + + app = QtWidgets.QApplication(sys.argv) + coordUI = CoordTransformUI(None) + coordUI.show() + ret = app.exec_() diff --git a/sampleview.py b/sampleview.py index e440dccd327f5955e7064a8caf15830b63ed0b69..5fc8b92abb6a98b35df5615cfa42d17f3a39f94a 100644 --- a/sampleview.py +++ b/sampleview.py @@ -37,6 +37,7 @@ from .analysis.particleEditor import ParticleEditor from .ramancom.configRaman import RamanConfigWin from .scenePyramid import ScenePyramid from .gepardlogging import setDefaultLoggingConfig +from .gui.coordTransferGUI import CoordTransformUI class SampleView(QtWidgets.QGraphicsView): @@ -83,7 +84,7 @@ class SampleView(QtWidgets.QGraphicsView): self.pyramid = ScenePyramid(self, self.logger) self.particleEditor = None self.fititems = [] - self.boundaryitems = [[],[]] + self.boundaryitems = [[], []] self.scanitems = [] self.ramanscanitems = [] self.particleInfoBox = None @@ -93,6 +94,7 @@ class SampleView(QtWidgets.QGraphicsView): self.selectedParticleIndices = [] self.seedPoints = [] self.particlePainter = None + self.coordTransferWidget = None self.detectionwidget = None self.ramanwidget = RamanScanUI(self.ramanctrl, None, self.logger, self) @@ -103,6 +105,7 @@ class SampleView(QtWidgets.QGraphicsView): self.setMinimumSize(600, 600) self.darkenPixmap = False self.microscopeMode = None + self.hiddenWidgets: list = [] self.update() @@ -141,10 +144,9 @@ class SampleView(QtWidgets.QGraphicsView): self.disconnectRaman() self.saveDataSet() event.accept() - self.oscanwidget.close() - if self.detectionwidget is not None: - self.detectionwidget.close() - self.ramanwidget.close() + for widget in [self.oscanwidget, self.detectionwidget, self.ramanwidget, self.coordTransferWidget]: + if widget is not None: + widget.close() else: event.ignore() @@ -257,6 +259,7 @@ class SampleView(QtWidgets.QGraphicsView): self.imgdata = None self.activateMaxMode(loadnew=True) self.imparent.snapshotAct.setEnabled(True) + self.imparent.recalculateCoordAct.setEnabled(True) self.setupLoggerToDataset() def importProject(self, fname): @@ -279,6 +282,7 @@ class SampleView(QtWidgets.QGraphicsView): self.imgdata = None self.activateMaxMode(loadnew=True) self.imparent.snapshotAct.setEnabled(True) + self.imparent.recalculateCoordAct.setEnabled(True) self.setupLoggerToDataset() def setupLoggerToDataset(self): @@ -362,26 +366,27 @@ class SampleView(QtWidgets.QGraphicsView): self.imparent.updateModes(self.mode, self.getMaxMode()) def mousePressEvent(self, event): - if event.button()==QtCore.Qt.MiddleButton: + if event.button() == QtCore.Qt.MiddleButton: self.drag = event.pos() - if self.particlePainter is None: - if event.button()==QtCore.Qt.LeftButton: + if self.particlePainter is not None: + self.particlePainter.mousePressEvent(event) + elif self.coordTransferWidget is not None: + self.coordTransferWidget.mousePressEvent(event) + else: + if event.button() == QtCore.Qt.LeftButton: if self.mode in ["OpticalScan", "RamanScan"] and event.modifiers()==QtCore.Qt.ControlModifier: p0 = self.mapToScene(event.pos()) self.moveStageToPosition(p0) - elif self.mode=="ParticleDetection": + elif self.mode == "ParticleDetection": p0 = self.mapToScene(event.pos()) self.detectionwidget.setImageCenter([p0.x(), p0.y()]) else: p0 = self.mapToScene(event.pos()) super(SampleView, self).mousePressEvent(event) - - else: - self.particlePainter.mousePressEvent(event) def mouseMoveEvent(self, event): if self.drag is not None: @@ -845,3 +850,21 @@ class SampleView(QtWidgets.QGraphicsView): for seedpoint in self.seedPoints: self.scene().removeItem(seedpoint) self.seedPoints = [] + + def recalculateCoordinateSystem(self): + for widget in [self.oscanwidget, self.detectionwidget, self.ramanwidget]: + if widget is not None: + if not widget.isHidden(): + self.hideWidget(widget) + + self.coordTransferWidget = CoordTransformUI(self) + self.coordTransferWidget.show() + + def hideWidget(self, widget: QtWidgets.QWidget) -> None: + self.hiddenWidgets.append(widget) + widget.hide() + + def showHiddenWidgets(self) -> None: + for widget in self.hiddenWidgets: + widget.show() + self.hiddenWidgets = [] \ No newline at end of file diff --git a/tests/test_coordTransfer.py b/tests/test_coordTransfer.py new file mode 100644 index 0000000000000000000000000000000000000000..5dcaf80cfde6028cdcc2882e0adafa7ff93a784c --- /dev/null +++ b/tests/test_coordTransfer.py @@ -0,0 +1,57 @@ +import unittest +import numpy as np +from ..coordinatetransform import getTransform + +from ..dataset import DataSet + + +def getRotMat(angles: tuple): + c1, s1 = np.cos(angles[0]), np.sin(angles[0]) + c2, s2 = np.cos(angles[1]), np.sin(angles[1]) + c3, s3 = np.cos(angles[2]), np.sin(angles[2]) + return np.mat([[c1 * c3 - s1 * c2 * s3, -c1 * s3 - s1 * c2 * c3, s1 * s2], + [s1 * c3 + c1 * c2 * s3, -s1 * s3 + c1 * c2 * c3, -c1 * s2], + [s1 * s3, s2 * c3, c2]]) + + +class TestCoordinateTransform(unittest.TestCase): + def test_transform(self): + srcPoints: np.ndarray = np.array([[1, 1, 0], + [1, -1, 3], + [-1, -1, 0], + [-1, 1, 0], + [0.2, 0.3, 0.2]]) + + angleDegrees: list = [0, -5, 5] + shifts: list = [[0, 0, 0], + [2, -3, 10], + [6, 2, -4]] + + # maxError: float = 0.0 + for angleX in angleDegrees: + for angleY in angleDegrees: + for angleZ in angleDegrees: + angles: tuple = (np.deg2rad(angleX), np.deg2rad(angleY), np.deg2rad(angleZ)) + # rotMat: np.ndarray = getRotMat(angles) + + T = getRotMat(angles).T.A + + for shift in shifts: + dstPoints: np.ndarray = np.zeros_like(srcPoints) + for i in range(srcPoints.shape[0]): + dstPoints[i, :] = (np.dot(srcPoints[i, :], T) + np.array(shift)) + + T, pc, zpc, accept = getTransform(srcPoints, dstPoints) + for i in range(srcPoints.shape[0]): + points = np.dot(srcPoints[i, :], T) + pc + error = np.linalg.norm(points-dstPoints[i, :]) + # if error > maxError: + # maxError = error + # print('new max error', maxError, angles, shift) + self.assertTrue(error < 1.0) + + def test_attributes(self): + # dataset has to have these attributes: + dset: DataSet = DataSet('test') + self.assertTrue(hasattr(dset, 'trayMarkers')) + self.assertTrue(hasattr(dset, 'imageMarkers'))