# -*- 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 . """ import os import cv2 import numpy as np from typing import Union, List, TYPE_CHECKING from PyQt5 import QtCore, QtWidgets, QtGui from scipy.optimize import least_squares from PIL import Image, ImageQt from .zeissxml import ZeissHandler, make_parser from ..gui.opticalscanui import PointCoordinates from ..helperfunctions import cv2imread_fix, cv2imwrite_fix from ..instrumentcom.instrumentConfig import defaultPath from ..dataset import DataSet from ..scenePyramid import ScenePyramid from ..coordTransform import CoordTransfer if TYPE_CHECKING: from .zeissxml import Region, ZRange, Marker class ImageImporter(QtWidgets.QDialog): ImportComplete: QtCore.pyqtSignal = QtCore.pyqtSignal(str) CoordinateSetupRequired: QtCore.pyqtSignal = QtCore.pyqtSignal() def __init__(self, instrctrl): super(ImageImporter, self).__init__() self.setWindowTitle("Create Project from Image") self.instrctrl = instrctrl self.region: Union['Region', None] = None self.zrange: Union['ZRange', None] = None self.markers: List['Marker'] = [] self.colorImg: Union[np.ndarray, None] = None self.zImg: Union[np.ndarray, None] = None self.xmlPath: str = '' self.optnLayout = QtWidgets.QVBoxLayout() self.ignoreMarkersCheckbox: QtWidgets.QCheckBox = QtWidgets.QCheckBox("Ignore Markers") self.ignoreMarkersCheckbox.setChecked(False) self.ignoreMarkersCheckbox.toggled.connect(self._toggleMarkers) self.fileSelector: FileSelector = FileSelector(self) self.pointgroup = QtWidgets.QGroupBox('Marker coordinates at Raman spot [µm]', self) emptyLayout = QtWidgets.QHBoxLayout() emptyLayout.addWidget(QtWidgets.QLabel('No markers loaded')) self.pointgroup.setLayout(emptyLayout) self.pointgroup.setDisabled(True) self.pconvert = QtWidgets.QPushButton('Convert', self) self.pexit = QtWidgets.QPushButton('Cancel', self) self.pconvert.released.connect(self.convert) self.pexit.released.connect(self.reject) self.pconvert.setEnabled(False) self.invertGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox('Invert Axis') invertLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() self.xinvert = QtWidgets.QCheckBox('x-axis') self.yinvert = QtWidgets.QCheckBox('y-axis') self.zinvert = QtWidgets.QCheckBox('z-axis') for checkbox in [self.xinvert, self.yinvert, self.zinvert]: invertLayout.addWidget(checkbox) invertLayout.addStretch() self.invertGroup.setLayout(invertLayout) self.invertGroup.setDisabled(True) btnLayout = QtWidgets.QHBoxLayout() btnLayout.addStretch() btnLayout.addWidget(self.pconvert) btnLayout.addWidget(self.pexit) self.blurGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox("Z-Image blur radius") self.blurspinbox = QtWidgets.QSpinBox(self) self.blurspinbox.setMinimum(3) self.blurspinbox.setMaximum(99) self.blurspinbox.setSingleStep(2) self.blurspinbox.setValue(5) self.blurspinbox.setFixedWidth(60) blurlayout = QtWidgets.QHBoxLayout() blurlayout.addWidget(self.blurspinbox) blurlayout.addStretch() self.blurGroup.setLayout(blurlayout) self.blurGroup.setDisabled(True) self.pxScaleGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox("Pixelscale (µm/px)") pxScaleLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() self.pxScaleGroup.setLayout(pxScaleLayout) self.pxScaleSpinbox: QtWidgets.QDoubleSpinBox = QtWidgets.QDoubleSpinBox() self.pxScaleSpinbox.setMinimum(0.01) self.pxScaleSpinbox.setMaximum(1000) self.pxScaleSpinbox.setValue(30.0) self.pxScaleSpinbox.setFixedWidth(60) pxScaleLayout.addWidget(self.pxScaleSpinbox) pxScaleLayout.addWidget(QtWidgets.QLabel("Only as first guess.\nThe coordinate system has to be calibrated properly afterwards.")) pxScaleLayout.addStretch() self.lblStatusImage: QtWidgets.QLabel = QtWidgets.QLabel() self.lblStatusZImage: QtWidgets.QLabel = QtWidgets.QLabel() self.lblStatusMeta: QtWidgets.QLabel = QtWidgets.QLabel() statusLayout: QtWidgets.QVBoxLayout = QtWidgets.QVBoxLayout() statusLayout.addWidget(self.lblStatusImage) statusLayout.addWidget(self.lblStatusZImage) statusLayout.addWidget(self.lblStatusMeta) self.optnLayout.addWidget(self.fileSelector) self.optnLayout.addWidget(self.pxScaleGroup) self.optnLayout.addWidget(self.pointgroup) self.optnLayout.addWidget(self.blurGroup) self.optnLayout.addWidget(self.invertGroup) self.optnLayout.addLayout(statusLayout) self.optnLayout.addLayout(btnLayout) self._imgPreview: ImagePreview = ImagePreview() hbox = QtWidgets.QHBoxLayout() hbox.addLayout(self.optnLayout) hbox.addWidget(self._imgPreview) self.setLayout(hbox) self._updateStatusLabels() self._enableDisableWidgets() def setColorImage(self, img: np.ndarray) -> None: self.colorImg = img self._imgPreview.setImage(img) self._updateStatusLabels() self._enableDisableWidgets() def setZValImage(self, zimg: np.ndarray) -> None: self.zImg = zimg self._updateStatusLabels() self._enableDisableWidgets() def readXML(self, xmlPath: str) -> None: self.xmlPath = xmlPath parser = make_parser() z = ZeissHandler() parser.setContentHandler(z) parser.parse(xmlPath) errmsges: List[str] = [] if len(z.markers) < 3: errmsges.append('Less than 3 markers found to adjust coordinates!') if None in [z.region.centerx, z.region.centery, z.region.width, z.region.height]: errmsges.append('Image dimensions incomplete or missing!') if None in [z.zrange.z0, z.zrange.zn, z.zrange.dz]: errmsges.append('ZStack information missing or incomplete!') if len(errmsges) > 0: QtWidgets.QMessageBox.critical(self, 'Error!', '\n'.join(errmsges), QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok) else: self.region = z.region self.zrange = z.zrange self.markers = z.markers self._updateStatusLabels() self._enableDisableWidgets() self._createPointGroup() def _createPointGroup(self) -> None: self._clearLayout(self.pointgroup.layout()) pointGroupInd: int = self.optnLayout.indexOf(self.pointgroup) self.optnLayout.removeWidget(self.pointgroup) self.pointgroup.setParent(None) self.pointgroup = QtWidgets.QGroupBox('Marker coordinates at Raman spot [µm]', self) self.points = PointCoordinates(len(self.markers), self.instrctrl, self, names=[m.name for m in self.markers]) self.points.pimageOnly.setVisible(False) self.points.readPoint.connect(self.takePoint) self.pointgroup.setLayout(self.points) self.optnLayout.insertWidget(pointGroupInd, self.ignoreMarkersCheckbox) self.optnLayout.insertWidget(pointGroupInd+1, self.pointgroup) @QtCore.pyqtSlot(float, float, float) def takePoint(self, x, y, z): points = self.points.getPoints() if len(points) >= 3: self.pconvert.setEnabled(True) def convert(self): fname = QtWidgets.QFileDialog.getSaveFileName(self, 'Create New GEPARD Project', defaultPath, '*.pkl')[0] colorImg: np.ndarray = self._imgPreview.getImage() if fname != '': dataset = DataSet(fname, newProject=True) requiresCoordTransfer = False if self.zImg is not None and self.markers is not None and self.zrange is not None: abort = self.convertZimg(dataset) if self.ignoreMarkersCheckbox.isChecked(): requiresCoordTransfer = True else: self._set_ZValImageToDataset(dataset, np.zeros(colorImg.shape[:2])) dataset.importedWithPlaceholderZValImg = True dataset.pixelscale_bf = dataset.pixelscale_df = self.pxScaleSpinbox.value() dataset.readin = False abort = False requiresCoordTransfer = True if not abort: cv2imwrite_fix(dataset.getImageName(), colorImg) ScenePyramid.createFromFullImage(dataset) dataset.save() self.ImportComplete.emit(dataset.getProjectFilePath()) if requiresCoordTransfer: self.CoordinateSetupRequired.emit() self.close() else: QtWidgets.QMessageBox.critical(self, "Error", "Failed importing project...") def convertZimg(self, dataset) -> bool: """ Converts the zvalImage and sets coordinate system, if marker shall be used. Returns whether or not to abort the import process. Can be True, if transform residues are too large.. """ if self.ignoreMarkersCheckbox.isChecked(): T, pc, zpc, accept = None, None, np.array([0, 0, 0]), True else: T, pc, zpc, accept = self.getTransform() newTransform: CoordTransfer = CoordTransfer() newTransform.rotMatrix = T newTransform.offset = pc dataset.coordinatetransform = newTransform # set image center as reference point (assume just one tile) # in data set (use Zeiss coordinates) p0center = np.array([self.region.centerx, self.region.centery]) - zpc[:2] dataset.lastpos = p0center dataset.readin = False N = int(round(abs(self.zrange.zn - self.zrange.z0) / self.zrange.dz)) z0, zn = self.zrange.z0, self.zrange.zn if zn < z0: zn, z0 = z0, zn # zeiss z axis has large values at the surface and smaller for large particles # that is why we need to invert the zpositions: zimg black corresponds to zn # zimg white corresponds to z0 dataset.zpositions = np.linspace(z0, zn, N)[::-1] - zpc[2] dataset.heightmapParams = np.zeros(3) dataset.signy = 1. zimg = self.zImg radius = self.blurspinbox.value() blur = cv2.GaussianBlur(zimg, (radius, radius), 0) blur = np.clip(blur * 255.0, 0, 255.0) blur[np.isnan(blur)] = 0. blur = np.uint8(blur) pshift = self.instrctrl.getSpectrumPositionShift() dataset.pshift = pshift pixelscale = self.region.scalex * 1.e6 # use input image as single image aquired in one shot dataset.pixelscale_df = dataset.pixelscale_bf = pixelscale dataset.zvalimg = blur dataset.calculateImageDimsFromZImg() self._set_ZValImageToDataset(dataset, blur) abort: bool = not accept return abort def _set_ZValImageToDataset(self, dataset: 'DataSet', zvalImg: np.ndarray) -> None: zimgname = dataset.getZvalImageName() cv2imwrite_fix(zimgname, zvalImg) dataset.zvalimg = "saved" def getTransform(self): points = self.points.getPoints() pshift = self.instrctrl.getSpectrumPositionShift() points[:, 0] -= pshift[0] points[:, 1] -= pshift[1] Parity = np.mat(np.diag([-1. if self.xinvert.isChecked() else 1., -1. if self.yinvert.isChecked() else 1., -1. if self.zinvert.isChecked() else 1.])) zpoints = np.array([m.getPos() for m in self.markers], dtype=np.double) 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 e = (np.dot(zpoints, T) - shift[np.newaxis, :] - pointsbest) print("Transformation angles:", optangles, flush=True) print("Transformation shift:", shift, flush=True) print("Transformation err:", e, flush=True) d = np.linalg.norm(e, axis=1) accept = True if np.any(d > 1.0): ret = QtWidgets.QMessageBox.warning(self, 'Warning!', f'Transformation residuals are large:{d}', QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Ok) if ret == QtWidgets.QMessageBox.Cancel: accept = False return T, pc - shift, zpc, accept def enableDisablePxScaleGroup(self, enabled: bool) -> None: self._clearLayout(self.pxScaleGroup.layout()) pxScaleGroupInd: int = self.optnLayout.indexOf(self.pxScaleGroup) self.optnLayout.removeWidget(self.pxScaleGroup) self.pxScaleGroup.setParent(None) layout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() if enabled: layout.addWidget(self.pxScaleSpinbox) else: layout.addWidget(QtWidgets.QLabel("Will be read from MetaFile")) layout.addStretch() self.pxScaleGroup: QtWidgets.QGroupBox = QtWidgets.QGroupBox("Pixelscale (µm/px)") self.pxScaleGroup.setLayout(layout) self.pxScaleGroup.setEnabled(enabled) self.optnLayout.insertWidget(pxScaleGroupInd, self.pxScaleGroup) def _enableDisableWidgets(self) -> None: enabled: bool = self.colorImg is not None self.pconvert.setEnabled(enabled) self.invertGroup.setEnabled(enabled) self.blurGroup.setEnabled(enabled) def _updateStatusLabels(self) -> None: if self.colorImg is None: self.lblStatusImage.setText("No Image loaded.") self.lblStatusImage.setStyleSheet("color: red") else: self.lblStatusImage.setText("Image loaded.") self.lblStatusImage.setStyleSheet("color: green") if self.zImg is None: self.lblStatusZImage.setStyleSheet("color: orange") self.lblStatusZImage.setText("No Z-Image loaded.") else: self.lblStatusZImage.setStyleSheet("color: green") self.lblStatusZImage.setText("Z-Image loaded.") if self.region is None or self.zrange is None or len(self.markers) == 0: self.lblStatusMeta.setStyleSheet("color: orange") self.lblStatusMeta.setText("No Metadata loaded.") else: self.lblStatusMeta.setStyleSheet("color: green") self.lblStatusMeta.setText("Metadata loaded.") def _toggleMarkers(self) -> None: """ Sets Marker group enabled or disabled, according to the checkbox. """ self.pointgroup.setDisabled(self.ignoreMarkersCheckbox.isChecked()) def _clearLayout(self, layout) -> None: """ Deletes all contents of the given layout. """ for i in reversed(range(layout.count())): item = layout.itemAt(i) if item is not None: widget = item.widget() layout.removeWidget(widget) else: layout.removeItem(item) class FileSelector(QtWidgets.QGroupBox): def __init__(self, parent: 'ImageImporter'): super(FileSelector, self).__init__() self._parent: 'ImageImporter' = parent self._layout: QtWidgets.QGridLayout = QtWidgets.QGridLayout() self.setLayout(self._layout) self._LineEditImage: QtWidgets.QLineEdit = QtWidgets.QLineEdit() self._LineEditZImage: QtWidgets.QLineEdit = QtWidgets.QLineEdit() self._LineEditMeta: QtWidgets.QLineEdit = QtWidgets.QLineEdit() self._BtnLoadImg: QtWidgets.QPushButton = QtWidgets.QPushButton("Load") self._BtnLoadZImg: QtWidgets.QPushButton = QtWidgets.QPushButton("Load") self._BtnLoadMeta: QtWidgets.QPushButton = QtWidgets.QPushButton("Load") self._createLayout() self._connectButtons() def _createLayout(self) -> None: boldFont: QtGui.QFont = QtGui.QFont() boldFont.setBold(True) labelImage: QtWidgets.QLabel = QtWidgets.QLabel('Color Image') labelZImg: QtWidgets.QLabel = QtWidgets.QLabel('Zvalue Image') labelMeta: QtWidgets.QLabel = QtWidgets.QLabel('Metadata') for lbl in [labelImage, labelZImg, labelMeta]: lbl.setFont(boldFont) for lineEdit in [self._LineEditImage, self._LineEditZImage, self._LineEditMeta]: lineEdit.setFixedWidth(200) lineEdit.setDisabled(True) self._layout.addWidget(labelImage, 0, 0) self._layout.addWidget(self._LineEditImage, 0, 1) self._layout.addWidget(self._BtnLoadImg, 0, 2) self._layout.addWidget(labelZImg, 1, 0) self._layout.addWidget(self._LineEditZImage, 1, 1) self._layout.addWidget(self._BtnLoadZImg, 1, 2) self._layout.addWidget(labelMeta, 2, 0) self._layout.addWidget(self._LineEditMeta, 2, 1) self._layout.addWidget(self._BtnLoadMeta, 2, 2) def _connectButtons(self) -> None: self._BtnLoadImg.released.connect(self._loadImage) self._BtnLoadZImg.released.connect(self._loadZImage) self._BtnLoadMeta.released.connect(self._loadMeta) def _loadImage(self) -> None: fname, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select Color Image", filter="Images (*.png *.jpg *.jpeg *.tiff *tif)") if fname: try: image: np.ndarray = cv2imread_fix(fname) print('loaded colorImg with shape:', image.shape) self._LineEditImage.setText(os.path.basename(fname)) self._parent.setColorImage(image) except Exception as e: QtWidgets.QMessageBox.warning(self, "Error", f"Image {fname} could not be loaded:/n{e}") def _loadZImage(self) -> None: fname, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select ZVal Image", filter="Images (*.tiff *tif)") if fname: try: zImage: np.ndarray = cv2imread_fix(fname, cv2.IMREAD_GRAYSCALE) print('loaded zImg with shape:', zImage.shape) self._LineEditZImage.setText(os.path.basename(fname)) print('set Text') self._parent.setZValImage(zImage) print('set zvalImage to Parent') except Exception as e: QtWidgets.QMessageBox.warning(self, "Error", f"ZImage {fname} could not be loaded:/n{e}") def _loadMeta(self) -> None: fname, _ = QtWidgets.QFileDialog.getOpenFileName(self, "Select Metadata File", filter="XML File (*.xml)") if fname: self._LineEditMeta.setText(os.path.basename(fname)) self._parent.readXML(fname) print('finished reading XML') self._parent.enableDisablePxScaleGroup(False) else: self._parent.enableDisablePxScaleGroup(True) class ImagePreview(QtWidgets.QWidget): def __init__(self): super(ImagePreview, self).__init__() self.prevSize: int = 400 self.image: Union[np.ndarray, None] = None self.btnRotLeft: QtWidgets.QPushButton = QtWidgets.QPushButton("Rotate Left") self.btnRotLeft.released.connect(self._rotateLeft) self.btnRotRight: QtWidgets.QPushButton = QtWidgets.QPushButton("Rotate Right") self.btnRotRight.released.connect(self._rotateRight) self.btnFlipHor: QtWidgets.QPushButton = QtWidgets.QPushButton("Flip Horiz.") self.btnFlipHor.released.connect(self._flipHorizontally) self.btnFlipVert: QtWidgets.QPushButton = QtWidgets.QPushButton("Flip Vert.") self.btnFlipVert.released.connect(self._flipVertically) btnLayout: QtWidgets.QHBoxLayout = QtWidgets.QHBoxLayout() for btn in [self.btnRotLeft, self.btnRotRight, self.btnFlipHor, self.btnFlipVert]: btn.setDisabled(True) btn.setFixedWidth(90) btnLayout.addWidget(btn) btnLayout.addStretch() self.prevLabel: QtWidgets.QLabel = QtWidgets.QLabel() layout = QtWidgets.QVBoxLayout() self.setLayout(layout) layout.addLayout(btnLayout) layout.addWidget(self.prevLabel) def setImage(self, img: np.ndarray) -> None: self.image = img self._updateLabelImage() for btn in [self.btnRotLeft, self.btnRotRight, self.btnFlipHor, self.btnFlipVert]: btn.setEnabled(True) def getImage(self) -> np.ndarray: return self.image def _updateLabelImage(self) -> None: if self.image is not None: factor = min([self.prevSize / self.image.shape[0], self.prevSize / self.image.shape[1]]) img = cv2.resize(self.image, None, fx=factor, fy=factor, interpolation=cv2.INTER_CUBIC) img = Image.fromarray(img, mode='RGB') qt_img = ImageQt.ImageQt(img) self.prevLabel.setPixmap(QtGui.QPixmap.fromImage(qt_img)) def _rotateLeft(self) -> None: try: self.image = cv2.rotate(self.image, cv2.ROTATE_90_COUNTERCLOCKWISE) self._updateLabelImage() except Exception as e: print(e) def _rotateRight(self) -> None: self.image = cv2.rotate(self.image, cv2.ROTATE_90_CLOCKWISE) self._updateLabelImage() def _flipHorizontally(self) -> None: self.image = np.fliplr(self.image) self._updateLabelImage() def _flipVertically(self) -> None: self.image = np.flipud(self.image) self._updateLabelImage() if __name__ == '__main__': import sys app = QtWidgets.QApplication(sys.argv) importer = ImageImporter(None) importer.show() app.exec()