coordTransferGUI.py 18 KB
Newer Older
1
from PyQt5 import QtWidgets, QtGui, QtCore
Josef Brandt's avatar
Josef Brandt committed
2
from typing import Tuple, TYPE_CHECKING, List
3
from copy import deepcopy
Josef Brandt's avatar
Josef Brandt committed
4

5
from ..coordTransform import *
Josef Brandt's avatar
Josef Brandt committed
6 7 8 9
if TYPE_CHECKING:
    from ..sampleview import SampleView
    from ..instrumentcom.instrumentComBase import InstrumentComBase
    from ..dataset import DataSet
10 11 12


class CoordTransformUI(QtWidgets.QWidget):
13
    ImageMarkerAdded: QtCore.pyqtSignal = QtCore.pyqtSignal(tuple)
Josef Brandt's avatar
Josef Brandt committed
14 15 16 17 18
    ImageMarkerDeleted: QtCore.pyqtSignal = QtCore.pyqtSignal(int)
    CenterOnMarker: QtCore.pyqtSignal = QtCore.pyqtSignal(int)
    WindowClosed: QtCore.pyqtSignal = QtCore.pyqtSignal()

    def __init__(self, viewParent: 'SampleView'):
19 20
        super(CoordTransformUI, self).__init__()
        self.setWindowTitle('Recalculate Coordinate System')
Josef Brandt's avatar
Josef Brandt committed
21 22 23 24
        self._viewParent: 'SampleView' = viewParent
        self._dataset: 'DataSet' = viewParent.dataset
        assert self._viewParent is not None
        self._instrctrl: 'InstrumentComBase' = self._viewParent.instrctrl
25
        self.coordTransf: CoordTransfer = CoordTransfer()
Josef Brandt's avatar
Josef Brandt committed
26
        self._transformNotYetSaved: bool = True
27

Josef Brandt's avatar
Josef Brandt committed
28 29 30 31
        self._imgMarkersSrc: List[ImageMarker] = []
        self._imgMarkersDst: List[ImageMarker] = []  # in the new coord system
        self._trayMarkersSrc: List[TrayMarker] = []
        self._trayMarkersDst: List[TrayMarker] = []  # in the new coord system
32

Josef Brandt's avatar
Josef Brandt committed
33 34
        self._layout = QtWidgets.QVBoxLayout()
        self.setLayout(self._layout)
35 36

        selectionLayout = QtWidgets.QHBoxLayout()
Josef Brandt's avatar
Josef Brandt committed
37
        self._trayMarkersBtn: QtWidgets.QRadioButton = QtWidgets.QRadioButton('Use Tray Markers')
38
        self._trayMarkersBtn.pressed.connect(self._updateTrayMarkersGUI)
Josef Brandt's avatar
Josef Brandt committed
39 40
        self._trayMarkersBtn.setChecked(False)
        self._imgMarkersBtn: QtWidgets.QRadioButton = QtWidgets.QRadioButton('Use Image Markers')
41
        self._imgMarkersBtn.pressed.connect(self._updateImageMarkersGUI)
Josef Brandt's avatar
Josef Brandt committed
42
        self._imgMarkersBtn.setChecked(True)
43

Josef Brandt's avatar
Josef Brandt committed
44 45
        selectionLayout.addWidget(self._trayMarkersBtn)
        selectionLayout.addWidget(self._imgMarkersBtn)
46

Josef Brandt's avatar
Josef Brandt committed
47 48 49
        self._addMarkerBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Add New Markers')
        self._addMarkerBtn.setStyleSheet("background-color : lightgrey")
        self._formatAsToggleBtn(self._addMarkerBtn)
50

Josef Brandt's avatar
Josef Brandt committed
51
        self._transferResultLbl: QtWidgets.QLabel = QtWidgets.QLabel('No valid input')
52

Josef Brandt's avatar
Josef Brandt committed
53 54
        self._trayMarkerGroup: QtWidgets.QGroupBox = self._getTrayMarkerGroup()
        self._imgMarkerGroup: QtWidgets.QGroupBox = self._getImageMarkerGroup()
55

56
        self._invertGroup: 'InvertAxisGroup' = InvertAxisGroup()
Josef Brandt's avatar
Josef Brandt committed
57
        self._invertGroup.CheckBoxToggled.connect(self._calculateCoordinateTransfer)
Josef Brandt's avatar
Josef Brandt committed
58

59
        saveBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Save to Dataset')
Josef Brandt's avatar
Josef Brandt committed
60 61 62 63
        saveBtn.released.connect(self._saveToDataset)
        btnLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout()
        btnLayout.addWidget(saveBtn)
        btnLayout.addStretch()
64

Josef Brandt's avatar
Josef Brandt committed
65 66
        self._layout.addLayout(selectionLayout)
        self._layout.addWidget(self._trayMarkerGroup)
67
        self._layout.addWidget(self._invertGroup)
Josef Brandt's avatar
Josef Brandt committed
68
        self._layout.addWidget(self._transferResultLbl)
Josef Brandt's avatar
Josef Brandt committed
69
        self._layout.addLayout(btnLayout)
Josef Brandt's avatar
Josef Brandt committed
70 71

        self._updateFromDataset()
72
        self._updateImageMarkersGUI()
73

Josef Brandt's avatar
Josef Brandt committed
74 75 76 77
    def _updateFromDataset(self) -> None:
        self._imgMarkersSrc = deepcopy(self._dataset.imageMarkers)
        for imgMarker in self._imgMarkersSrc:
            newImgMarker: ImageMarker = ImageMarker()
Josef Brandt's avatar
Josef Brandt committed
78
            newImgMarker.index = imgMarker.index
Josef Brandt's avatar
Josef Brandt committed
79 80
            newImgMarker.imgCoordX = imgMarker.imgCoordX
            newImgMarker.imgCoordY = imgMarker.imgCoordY
Josef Brandt's avatar
Josef Brandt committed
81

Josef Brandt's avatar
Josef Brandt committed
82
            self._imgMarkersDst.append(newImgMarker)
83

Josef Brandt's avatar
Josef Brandt committed
84 85 86 87
        self._trayMarkersSrc = deepcopy(self._dataset.trayMarkers)
        for _ in range(len(self._trayMarkersSrc)):
            newTrayMarker: TrayMarker = TrayMarker()
            self._trayMarkersDst.append(newTrayMarker)
88 89 90 91 92 93 94 95 96 97

    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)
Josef Brandt's avatar
Josef Brandt committed
98
        layout.addWidget(QtWidgets.QLabel("NOT YET IMPLEMENTED"))
99 100 101 102 103 104 105 106 107
        return group

    def _getImageMarkerGroup(self) -> QtWidgets.QGroupBox:
        def makeDeleteLambda(ind):
            return lambda: self._deleteImageMarker(ind)
        
        def makeReadNewPosLambda(ind):
            return lambda: self._readNewImageMarker(ind)

Josef Brandt's avatar
Josef Brandt committed
108 109 110
        def makeCenterOnLambda(ind):
            return lambda: self.CenterOnMarker.emit(ind)

111 112 113
        layout = QtWidgets.QGridLayout()
        group: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Image Based Markers')
        group.setLayout(layout)
Josef Brandt's avatar
Josef Brandt committed
114 115
        layout.addWidget(self._addMarkerBtn)
        if len(self._imgMarkersSrc) == 0:
116 117 118
            layout.addWidget(QtWidgets.QLabel('No image markers found.'))
        else:
            layout.addWidget(QtWidgets.QLabel('Number'), 1, 0)
Josef Brandt's avatar
Josef Brandt committed
119 120 121
            layout.addWidget(QtWidgets.QLabel('Old Coordinates'), 1, 1)
            layout.addWidget(QtWidgets.QLabel('New Coordinates'), 1, 2)

122 123 124 125
            for index, imgMarkerSrc in enumerate(self._imgMarkersSrc):
                newMarkerDst: ImageMarker = self._imgMarkersDst[index]
                newX, newY, newZ = newMarkerDst.worldCoordX, newMarkerDst.worldCoordY, newMarkerDst.worldCoordZ
                x, y, z = imgMarkerSrc.worldCoordX, imgMarkerSrc.worldCoordY, imgMarkerSrc.worldCoordZ
126
                if np.any(np.isnan([x, y, z])):
127 128 129
                    x, y, z = 'Not defined', 'Not defined', 'Not defined'
                else:
                    x, y, z = round(x), round(y), round(z)
130

131
                row: int = 2+index
132 133 134
                updateBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Read Coordinates')
                updateBtn.clicked.connect(makeReadNewPosLambda(index))
                focusBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Center On')
Josef Brandt's avatar
Josef Brandt committed
135
                focusBtn.clicked.connect(makeCenterOnLambda(index))
136 137
                delBtn: QtWidgets.QPushButton = QtWidgets.QPushButton('Delete')
                delBtn.clicked.connect(makeDeleteLambda(index))
138
                layout.addWidget(QtWidgets.QLabel(str(imgMarkerSrc.index+1)), row, 0)
139
                layout.addWidget(QtWidgets.QLabel(f'x: {x} µm\ny: {y} µm\nz: {z} µm'), row, 1)
140 141 142 143
                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)
144 145 146 147 148
                layout.addWidget(updateBtn, row, 3)
                layout.addWidget(focusBtn, row, 4)
                layout.addWidget(delBtn, row, 5)
        return group

Josef Brandt's avatar
Josef Brandt committed
149 150 151 152 153
    def _resetMarkerIndices(self) -> None:
        for index, (imgMarker, newImgMarker) in enumerate(zip(self._imgMarkersSrc, self._imgMarkersDst)):
            imgMarker.index = index
            newImgMarker.index = index

154
    def _calculateCoordinateTransfer(self) -> None:
Josef Brandt's avatar
Josef Brandt committed
155 156
        dstPoints, srcPoints = self._getSourceAndDestinationPoints()
        invertX, invertY, invertZ = self._invertGroup.getInvert()
Josef Brandt's avatar
Josef Brandt committed
157

Josef Brandt's avatar
Josef Brandt committed
158
        if len(srcPoints) == len(dstPoints) and len(srcPoints) >= 3 and np.all(np.isfinite(dstPoints)):
Josef Brandt's avatar
Josef Brandt committed
159
            if not self._dataset.coordinateSystemConfigured:
160
                self._setOriginalCoordSystem()
Josef Brandt's avatar
Josef Brandt committed
161 162 163 164 165
                srcPoints = self._setAndGetCreatedImgMarkerSourcePoints()

            if np.any(np.isnan(srcPoints)):
                self._viewParent.getLogger().warning("Corrdinate transfer cannot be done, source markers contain NaN")
                return
Josef Brandt's avatar
Josef Brandt committed
166

Josef Brandt's avatar
Josef Brandt committed
167 168
            try:
                self.coordTransf: CoordTransfer = getTransform(srcPoints, dstPoints, invertX, invertY, invertZ)
Josef Brandt's avatar
Josef Brandt committed
169 170 171 172 173
                errorString: str = "Transform errors:"
                for i, error in enumerate(self.coordTransf.error, start=1):
                    errorString += f" Marker {i}: {round(error, 1)} µm,"
                self._transferResultLbl.setText(errorString)
                self._transformNotYetSaved = True
Josef Brandt's avatar
Josef Brandt committed
174 175 176 177
            except ValueError as e:
                QtWidgets.QMessageBox.critical(self, "Transform failed", f"The following error occured:\n\n{e}\n\n"
                                                                         f"Please try slightly different coordinates.")
                self._dataset.invalidateCoordinateSystem()
178
        else:
Josef Brandt's avatar
Josef Brandt committed
179
            self._transferResultLbl.setText('No valid inputs for coordinate transfer')
180

Josef Brandt's avatar
Josef Brandt committed
181 182 183 184 185 186 187 188 189 190 191 192 193
    def _getSourceAndDestinationPoints(self) -> Tuple[np.ndarray, np.ndarray]:
        """
        Retrieves the currently defined point coordinates for source and destination coordinate system.
        Returns tuple of srcPoints (Nx3) array and dstPoints (Nx3) array of (x, y, z) coordinates for N markers.
        """
        oldMarkers: list = self._trayMarkersSrc if self._trayMarkersBtn.isChecked() else self._imgMarkersSrc
        newMarkers: list = self._trayMarkersDst if self._trayMarkersBtn.isChecked() else self._imgMarkersDst
        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])
        return dstPoints, srcPoints

194 195 196 197 198
    def _setOriginalCoordSystem(self) -> None:
        """
        In case no original coordinate system exists yet, a default one is created as a base for the coordinate
        transfer.
        """
Josef Brandt's avatar
Josef Brandt committed
199 200 201 202 203 204
        if not self._imgMarkersBtn.isChecked():
            QtWidgets.QMessageBox.critical(self, "Error", "A new coordinate system has to be created."
                                                          "This only works for image markers, please check the"
                                                          "according button!")
            return

205
        self._dataset.pshift = self._instrctrl.getSpectrumPositionShift()
Josef Brandt's avatar
Josef Brandt committed
206 207
        self._dataset.signx = 1.0
        self._dataset.signy = -1.0
Josef Brandt's avatar
Josef Brandt committed
208 209 210
        pxScale: float = getPixelScaleFromImageMarkers(self._imgMarkersDst)
        self._dataset.pixelscale_df = self._dataset.pixelscale_bf = pxScale
        self._dataset.calculateImageDimsFromZImg()
211

Josef Brandt's avatar
Josef Brandt committed
212
        dstPointsWorld: np.ndarray = np.array(
213 214
            [[marker.worldCoordX, marker.worldCoordY, marker.worldCoordZ] for marker in self._imgMarkersDst])

Josef Brandt's avatar
Josef Brandt committed
215 216 217 218 219
        self._dataset.fitpoints = dstPointsWorld
        self._dataset.calculateHeightmapParams(self._instrctrl.getSoftwareZ(), self._instrctrl.getUserZ())

        imgDim: np.ndarray = self._dataset.imagedim_df
        signx, signy = self._dataset.signx, self._dataset.signy
Josef Brandt's avatar
Josef Brandt committed
220
        self._dataset.lastpos = np.array([imgDim[0]/2 * signx, imgDim[1]/2 * signy])
Josef Brandt's avatar
Josef Brandt committed
221 222 223 224 225 226 227 228 229
        self._dataset.coordinatetransform = None

    def _setAndGetCreatedImgMarkerSourcePoints(self) -> np.ndarray:
        """
        Fake (but realistic in terms of scale) source coordinates are set for the image markers. We just assume
        world (0, 0, 0) to align with image (0, 0).
        All offsets and rotations will then be handled by the actual coordinate transfer.
        """
        for imgMarker in self._imgMarkersSrc:
Josef Brandt's avatar
Josef Brandt committed
230 231 232 233
            x, y, z = self._dataset.mapToLengthWithoutCoordTransfer((imgMarker.imgCoordX, imgMarker.imgCoordY))
            imgMarker.worldCoordX = x
            imgMarker.worldCoordY = y
            imgMarker.worldCoordZ = z
Josef Brandt's avatar
Josef Brandt committed
234 235 236 237 238

        srcPoints = np.array([[marker.worldCoordX, marker.worldCoordY, marker.worldCoordZ] for marker in self._imgMarkersSrc])
        assert not np.any(np.isnan(srcPoints))
        self._updateImageMarkersGUI(autoUpdateCoordTransfer=False)
        return srcPoints
239

240 241 242 243 244 245 246 247 248
    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")

249
    def _updateTrayMarkersGUI(self) -> None:
250 251 252
        """
        Use markers on the sample tray that are NOT in the microscope image.
        """
Josef Brandt's avatar
Josef Brandt committed
253 254 255 256
        self._imgMarkerGroup.setParent(None)
        self._trayMarkerGroup.setParent(None)
        self._trayMarkerGroup = self._getTrayMarkerGroup()
        self._layout.insertWidget(1, self._trayMarkerGroup)
257

Josef Brandt's avatar
Josef Brandt committed
258
    def _updateImageMarkersGUI(self, autoUpdateCoordTransfer: bool = True) -> None:
259 260
        """
        Use markers that are defined through characteristic spots within the microscope image.
Josef Brandt's avatar
Josef Brandt committed
261
        :param autoUpdateCoordTransfer: Also update the coordinate transfer
262
        """
Josef Brandt's avatar
Josef Brandt committed
263 264 265 266
        self._imgMarkerGroup.setParent(None)
        self._trayMarkerGroup.setParent(None)
        self._imgMarkerGroup = self._getImageMarkerGroup()
        self._layout.insertWidget(1, self._imgMarkerGroup)
Josef Brandt's avatar
Josef Brandt committed
267 268
        if autoUpdateCoordTransfer:
            self._calculateCoordinateTransfer()
269

270
    def mousePressed(self, event: QtGui.QMouseEvent) -> bool:
Josef Brandt's avatar
Josef Brandt committed
271
        wasConsumed: bool = False
Josef Brandt's avatar
Josef Brandt committed
272 273
        if self._addMarkerBtn.isChecked() and event.button() == QtCore.Qt.LeftButton:
            scenePos = self._viewParent.mapToScene(event.pos())
274
            self._addNewImageMarker((scenePos.x(), scenePos.y()))
Josef Brandt's avatar
Josef Brandt committed
275 276
            wasConsumed = True
        return wasConsumed
277 278

    def _deleteImageMarker(self, markerIndex: int) -> None:
Josef Brandt's avatar
Josef Brandt committed
279 280 281 282
        self._imgMarkersSrc.remove(self._imgMarkersSrc[markerIndex])
        self._imgMarkersDst.remove(self._imgMarkersDst[markerIndex])
        self.ImageMarkerDeleted.emit(markerIndex)
        self._resetMarkerIndices()
283
        self._updateImageMarkersGUI()
284 285

    def _addNewImageMarker(self, pos: tuple) -> None:
286 287 288 289
        newImageMarkerScr: ImageMarker = ImageMarker()
        newImageMarkerScr.index = len(self._imgMarkersSrc)
        newImageMarkerScr.imgCoordX = pos[0]
        newImageMarkerScr.imgCoordY = pos[1]
290 291 292 293 294 295 296 297

        try:
            x, y, z = self._dataset.mapToLengthWithoutCoordTransfer(pos, self._viewParent.microscopeMode)
            newImageMarkerScr.worldCoordX = x
            newImageMarkerScr.worldCoordY = y
            newImageMarkerScr.worldCoordZ = z
        except TypeError:
            pass  # getting world coordinates was not possible due to not yet configured coordinate system
298
        self._imgMarkersSrc.append(newImageMarkerScr)
299

300 301 302 303
        newImageMarkerDst: ImageMarker = ImageMarker()
        newImageMarkerDst.imgCoordX = pos[0]
        newImageMarkerDst.imgCoordY = pos[1]
        self._imgMarkersDst.append(newImageMarkerDst)
304

305 306
        self.ImageMarkerAdded.emit(pos)
        self._updateImageMarkersGUI()
307 308 309

    def _readNewImageMarker(self, index: int) -> None:
        """Reads current instrument coordinates and assignes them to the respective "new image marker" """
Josef Brandt's avatar
Josef Brandt committed
310 311 312 313
        x, y, z = self._instrctrl.getPosition()
        self._imgMarkersDst[index].worldCoordX = x
        self._imgMarkersDst[index].worldCoordY = y
        self._imgMarkersDst[index].worldCoordZ = z
314
        self._updateImageMarkersGUI()
315 316

    def _saveToDataset(self) -> None:
Josef Brandt's avatar
Josef Brandt committed
317 318 319 320 321 322
        if self._transformNotYetSaved:
            if self._imgMarkersBtn.isChecked():
                self._dataset.saveNewImageMarkers(self._imgMarkersSrc)
                self._viewParent.logger.debug(f"saved image markers to dataset")
            else:
                raise NotImplementedError
323

Josef Brandt's avatar
Josef Brandt committed
324 325 326 327 328 329
            save: bool = True
            if np.any(self.coordTransf.error > 10.0):
                reply = QtWidgets.QMessageBox.question(self, "Warning", f"Coordinate transfer residues are large ({np.round(self.coordTransf.error)} µm)! \n"
                                                                        "Really save the coordinte transfer result?",
                                                       QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No,
                                                       QtWidgets.QMessageBox.No)
Josef Brandt's avatar
Josef Brandt committed
330

Josef Brandt's avatar
Josef Brandt committed
331
                save = reply == QtWidgets.QMessageBox.Yes
Josef Brandt's avatar
Josef Brandt committed
332

Josef Brandt's avatar
Josef Brandt committed
333 334 335 336 337 338
            if save:
                self._dataset.coordinatetransform = self.coordTransf
                self._viewParent.logger.info(f"Saved coordinate transfer with rotMat {self.coordTransf.rotMatrix},"
                                             f"offset {self.coordTransf.offset} and center {self.coordTransf.rotationCenter}."
                                             f"Errors are: {self.coordTransf.error}")
                self._transformNotYetSaved = False
339 340 341 342 343 344 345 346 347

    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()

Josef Brandt's avatar
Josef Brandt committed
348
        self.WindowClosed.emit()
349

Josef Brandt's avatar
Josef Brandt committed
350
        del self
351 352 353


class InvertAxisGroup(QtWidgets.QGroupBox):
Josef Brandt's avatar
Josef Brandt committed
354 355
    CheckBoxToggled: QtCore.pyqtSignal = QtCore.pyqtSignal()

356 357 358 359 360 361
    def __init__(self):
        super(InvertAxisGroup, self).__init__()
        self._invertX: QtWidgets.QCheckBox = QtWidgets.QCheckBox()
        self._invertY: QtWidgets.QCheckBox = QtWidgets.QCheckBox()
        self._invertZ: QtWidgets.QCheckBox = QtWidgets.QCheckBox()

Josef Brandt's avatar
Josef Brandt committed
362 363 364
        for checkbox in [self._invertX, self._invertY, self._invertZ]:
            checkbox.stateChanged.connect(lambda: self.CheckBoxToggled.emit())

365 366 367 368 369 370 371
        layout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout()
        layout.addWidget(QtWidgets.QLabel("Invert X:"))
        layout.addWidget(self._invertX)
        layout.addWidget(QtWidgets.QLabel(", Invert Y:"))
        layout.addWidget(self._invertY)
        layout.addWidget(QtWidgets.QLabel(", Invert Z:"))
        layout.addWidget(self._invertZ)
372
        layout.addStretch()
373 374 375 376 377

        self.setLayout(layout)

    def getInvert(self) -> Tuple[bool, bool, bool]:
        return self._invertX.isChecked(), self._invertY.isChecked(), self._invertZ.isChecked()