From 819efb02507040688e41947e5015d82f4cb08b6b Mon Sep 17 00:00:00 2001 From: Robert Ohmacht Date: Thu, 14 Nov 2019 15:56:02 +0100 Subject: [PATCH] -initial integration of ScenePyramid from GepardEvaluation into Gepard --- dataset.py | 15 + detectionview.py | 91 ++- legacyConvert.py | 9 +- opticalbackground.py | 2 +- opticalscan.py | 91 ++- sampleview.py | 1682 +++++++++++++++++++++--------------------- scenePyramid.py | 899 ++++++++++++++++++++++ 7 files changed, 1906 insertions(+), 883 deletions(-) create mode 100644 scenePyramid.py diff --git a/dataset.py b/dataset.py index d823812..1846794 100644 --- a/dataset.py +++ b/dataset.py @@ -135,6 +135,9 @@ class DataSet(object): self.coordinatetransform = None # if imported form extern source coordinate system may be rotated self.signx = 1. self.signy = -1. + + # tiling parameters + self.pyramidParams = None # parameters specifically for raman scan self.pshift = None # shift of raman scan position relative to image center @@ -169,6 +172,12 @@ class DataSet(object): def __eq__(self, other): return recursiveDictCompare(self.__dict__, other.__dict__) + + def getPyramidParams(self): + return self.pyramidParams + + def setPyramidParams(self, pyramid_params): + self.pyramidParams = pyramid_params def getPixelScale(self, mode=None): if mode is None: @@ -275,6 +284,12 @@ class DataSet(object): self.__dict__.update(loadData(fname).__dict__) return fname + def getTilePath(self): + scandir = os.path.join(self.path, "tiles") + if not os.path.exists(scandir): + os.mkdir(scandir) + return scandir + def getScanPath(self): scandir = os.path.join(self.path, "scanimages") if not os.path.exists(scandir): diff --git a/detectionview.py b/detectionview.py index 4fb53ba..e1adde4 100644 --- a/detectionview.py +++ b/detectionview.py @@ -27,6 +27,7 @@ from threading import Thread from .segmentation import Segmentation from .analysis.particleCharacterization import getParticleStatsWithPixelScale, loadZValImageFromDataset from .errors import InvalidParticleError +from .scenePyramid import ScenePyramid Nscreen = 1000 @@ -269,14 +270,15 @@ class ParticleDetectionView(QtWidgets.QWidget): imageUpdate = QtCore.pyqtSignal(str, name='imageUpdate') #str = 'df' (= darkfield) or 'bf' (=bright field) detectionFinished = QtCore.pyqtSignal(name='detectionFinished') - def __init__(self, img, dataset, parent=None): + def __init__(self, pyramid: ScenePyramid, dataset, parent=None): super().__init__(parent, QtCore.Qt.Window) self.dataset = self.verifySeedpoints(dataset) - self.img = img - self.imgclip = 0,0,0,0 + self.pyramid = pyramid + self.img = pyramid.getFullImage(.5) + self.imgclip = 0, 0, 0, 0 self.seg = Segmentation(self.dataset, self) self.thread = None - self.view = parent + self.view : QtWidgets.QGraphicsView = parent vbox = QtWidgets.QVBoxLayout() hbox = QtWidgets.QHBoxLayout() @@ -529,33 +531,69 @@ class ParticleDetectionView(QtWidgets.QWidget): self.updateImageSeeds() def setImageCenter(self, center=None): + ''' + as more than the tiles are part of the scene + bounding rect may be bigger than just a rect around the tiles + tiles should be grouped + + ''' + width, height = self.pyramid.getBoundingRectDim() + if center is None: - centerx = self.img.shape[1]//2 - centery = self.img.shape[0]//2 + centerx = width//2 + centery = height//2 else: centerx, centery = center - if self.img.shape[0]self.img.shape[0]: - n1 -= n2-self.img.shape[0] - if n1<0: n1 = 0 - n2 = self.img.shape[0] - if m1<0: - m2 -= m1 - m1 = 0 - if m2>self.img.shape[1]: - m1 -= m2-self.img.shape[1] - if m1<0: m1 = 0 - m2 = self.img.shape[1] + + y1, y2 = centery-Nscreen//2, centery+Nscreen//2+1 + x1, x2 = centerx-Nscreen//2, centerx+Nscreen//2+1 + if y1 < 0: + y2 -= y1 + y1 = 0 + if y2 > height: + y1 -= y2-height + if y1 < 0: + y1 = 0 + y2 = height + if x1 < 0: + x2 -= x1 + x1 = 0 + if x2 > width: + x1 -= x2-width + if x1 < 0: + x1 = 0 + x2 = width + + self.imgclip = int(y1), int(y2), int(x1), int(x2) + self.subimg, pix = self.pyramid.getSubImage(self.imgclip) + ''' + img = QtGui.QImage(x2 - x1, y2 - y1, QtGui.QImage.Format_RGB888) + self.view.scene().render( + QtGui.QPainter(img), + QtCore.QRectF(0, 0, img.width(), img.height()), + #QtCore.QRectF(x1 + x0, y1 + y0, x2 - x1, y2 - y1) + QtCore.QRectF(x1, y1, x2 - x1, y2 - y1) + ) + + #pix = self.view.grab(QtCore.QRect(x1 + x0, y1 + y0, x2 - x1, y2 - y1)) + # @see https://stackoverflow.com/a/11399959 + #img = pix.toImage().convertToFormat(QtGui.QImage.Format_RGB888) + ptr = img.bits() + #ptr.setsize(img.byteCount()) + ptr.setsize(img.height() * img.width() * 3) + self.subimg = np.asarray(ptr).reshape(img.height(), img.width(), 3) + pix = QtGui.QPixmap() + pix.convertFromImage(img) + ''' + ''' sub = self.img[n1:n2,m1:m2,:].copy() self.imgclip = n1,n2,m1,m2 self.subimg = sub @@ -564,6 +602,7 @@ class ParticleDetectionView(QtWidgets.QWidget): pix = QtGui.QPixmap() pix.convertFromImage(QtGui.QImage(sub.data, width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) + ''' self.imglabel.clearData() self.imglabel.setPixmap(pix) self.updateImageSeeds() diff --git a/legacyConvert.py b/legacyConvert.py index 34c5d59..583aad9 100644 --- a/legacyConvert.py +++ b/legacyConvert.py @@ -27,8 +27,9 @@ from .helperfunctions import cv2imread_fix, cv2imwrite_fix from .analysis.particleContainer import ParticleContainer from .analysis import particleCharacterization as pc from .errors import InvalidParticleError +from .scenePyramid import ScenePyramid -currentVersion = 4 +currentVersion = 5 def legacyConversion(dset, recreatefullimage=False): if dset.version==0: @@ -110,6 +111,12 @@ def legacyConversion(dset, recreatefullimage=False): removeLegacyAttributes(dset) dset.version = 4 dset.save() + + if dset.version == 4: + print("Converting legacy version 4 to 5") + ScenePyramid.createFromFullImage(dset) + dset.version = 5 + dset.save() # add later conversion for higher version numbers here diff --git a/opticalbackground.py b/opticalbackground.py index 6cb9d4d..86342c5 100644 --- a/opticalbackground.py +++ b/opticalbackground.py @@ -106,7 +106,7 @@ class BackGroundManager(QtWidgets.QWidget): p0 = [points[:,0].min(), points[:,1].max()] p1 = [points[:,0].max(), points[:,1].min()] - reply = QtWidgets.QMessageBox.question(self, 'Message',f"The stage will move {round(3*width)} in x and {round(3*height)} in y.\nContinue?", + reply = QtWidgets.QMessageBox.question(self, 'Message', f"The stage will move {round(3*width)} in x and {round(3*height)} in y.\nContinue?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) if reply == QtWidgets.QMessageBox.Yes: fullimg = None diff --git a/opticalscan.py b/opticalscan.py index 3dafba5..ae17a88 100644 --- a/opticalscan.py +++ b/opticalscan.py @@ -31,6 +31,7 @@ from time import time import datetime from .opticalbackground import BackGroundManager from .zlevelsetter import ZLevelSetter +from .scenePyramid import ScenePyramid def scan(path, sol, zpositions, grid, controlclass, dataqueue, stopevent, logpath='', ishdr=False): @@ -84,10 +85,10 @@ def scan(path, sol, zpositions, grid, controlclass, dataqueue, def subtractBackground(image, background): avg = np.mean(background) - subtracted = np.clip(np.array(image - background + avg, dtype = np.uint8), 0, 255) + subtracted = np.clip(np.array(image - background + avg, dtype=np.uint8), 0, 255) return subtracted -def loadAndPasteImage(srcnames, fullimage, fullzval, width, height, +def loadAndPasteImage(srcnames, pyramid, fullzval, width, height, rotationvalue, p0, p1, p, background=None): colimgs = [] for name in srcnames: @@ -102,15 +103,22 @@ def loadAndPasteImage(srcnames, fullimage, fullzval, width, height, c, s = np.cos(np.radians(rotationvalue)), np.sin(np.radians(rotationvalue)) dx, dy = (x-p0[0])/width*img.shape[1], (p0[1]-y)/height*img.shape[0] - M = np.float32([[c,s,dx],[-s,c,dy]]) - if fullimage is not None: - cv2.warpAffine(img, M, (Nx, Ny), fullimage, borderMode=cv2.BORDER_TRANSPARENT) - cv2.warpAffine(zval, M, (Nx, Ny), fullzval, borderMode=cv2.BORDER_TRANSPARENT) - dst = fullimage - zval = fullzval - else: - dst = cv2.warpAffine(img, M, (Nx, Ny)) - zval = cv2.warpAffine(zval, M, (Nx, Ny)) + M = np.float32([[c, s, dx], [-s, c, dy]]) + dst = None + + if pyramid is not None: + if fullzval is not None: + cv2.warpAffine(zval, M, (Nx, Ny), fullzval, borderMode=cv2.BORDER_TRANSPARENT) + zval = fullzval + else: + zval = cv2.warpAffine(zval, M, (Nx, Ny)) + + pyramid.addSrcTile( + img, + np.float32([[c, s, 0], [-s, c, 0]]), + (dx, dy), + (Nx, Ny) + ) return dst, zval @@ -231,12 +239,13 @@ class OpticalScan(QtWidgets.QWidget): def __init__(self, ramanctrl, dataset, logpath='', parent=None): super().__init__(parent, QtCore.Qt.Window) self.logpath = logpath - self.view = parent + self.view: QtWidgets.QGraphicsView = parent mainLayout = QtWidgets.QVBoxLayout() pointgroup = QtWidgets.QGroupBox("Point coordinates [µm]", self) self.ramanctrl = ramanctrl self.dataset = dataset + self.pyramid: ScenePyramid = None self.positions = [] self.process = None self.points = PointCoordinates(5, self.ramanctrl, self) @@ -425,6 +434,9 @@ class OpticalScan(QtWidgets.QWidget): self.boundaryUpdate.emit() self.prun.setEnabled(True) + def setPyramid(self, pyramid): + self.pyramid = pyramid + def resetDataset(self, ds): self.dataset = ds self.points.createWidgets(5, list(zip(ds.fitindices,ds.fitpoints))) @@ -482,22 +494,34 @@ class OpticalScan(QtWidgets.QWidget): Nx, Ny = int((p1[0]-p0[0]+width)/width*img.shape[1]), int((p0[1]-p1[1]+height)/height*img.shape[0]) + 10 # + 10 because of rotation and hopefully it will be small c, s = np.cos(np.radians(rotationvalue)), np.sin(np.radians(rotationvalue)) dx, dy = (x-p0[0])/width*img.shape[1], (p0[1]-y)/height*img.shape[0] - M = np.float32([[c,s,dx],[-s,c,dy]]) - dst = cv2.warpAffine(img, M, (Nx, Ny)) - if self.view.imgdata is not None and self.dataset.lastpos is not None: - lp = self.dataset.lastpos - dx, dy = (lp[0]-p0[0])/width*img.shape[1], (p0[1]-lp[1])/height*img.shape[0] - full = self.view.imgdata - M = np.float32([[1,0,dx],[0,1,dy]]) - try: - full = cv2.warpAffine(full, M, (Nx, Ny)) #fails, if image dimensions are >32767x32767px... - dst = cv2.max(full, dst) - except: - QtWidgets.QMessageBox.critical(self, 'Error', 'Image is too large\nSelect smaller region.') - raise - return + M = np.float32([[c, s, dx], [-s, c, dy]]) + #dst = cv2.warpAffine(img, M, (Nx, Ny)) + M_rot = np.float32([[c, s, 0], [-s, c, 0]]) + img_rot = cv2.warpAffine(img, M_rot, (img.shape[1], img.shape[0] + 10)) + # calc new pixel start coords for tile + # @see https://stackoverflow.com/a/43166421/9880753 + c_dest = cv2.transform(np.float32([[(0, 0)]]), np.float32([[c, s, dx], [-s, c, dy]])) + + self.pyramid.addSrcTileSimple( + img_rot, + (c_dest[0][0][0], c_dest[0][0][1]), + p0 + ) + + #if self.view.imgdata is not None and self.dataset.lastpos is not None: + # lp = self.dataset.lastpos + # dx, dy = (lp[0]-p0[0])/width*img.shape[1], (p0[1]-lp[1])/height*img.shape[0] + # full = self.view.imgdata + # M = np.float32([[1,0,dx],[0,1,dy]]) + # try: + # full = cv2.warpAffine(full, M, (Nx, Ny)) #fails, if image dimensions are >32767x32767px... + # dst = cv2.max(full, dst) + # except: + # QtWidgets.QMessageBox.critical(self, 'Error', 'Image is too large\nSelect smaller region.') + # raise + # return - self.view.imgdata = dst + #self.view.imgdata = dst self.dataset.lastpos = p0 self.dataset.maxdim = p0 + p1 self.dataset.readin = False @@ -621,7 +645,8 @@ class OpticalScan(QtWidgets.QWidget): self.progressbar.setEnabled(True) self.progressbar.setRange(0, len(self.dataset.grid)) self.progressbar.setValue(0) - self.view.imgdata = None + #self.view.imgdata = None + self.pyramid.resetScene() self.view.blockUI() grid = np.asarray(self.dataset.grid) p0 = [grid[:,0].min(), grid[:,1].max()] @@ -657,7 +682,7 @@ class OpticalScan(QtWidgets.QWidget): else: background_img = None - self.view.imgdata, self.dataset.zvalimg = loadAndPasteImage(names, self.view.imgdata, self.dataset.zvalimg, width, height, + self.view.imgdata, self.dataset.zvalimg = loadAndPasteImage(names, self.pyramid, self.dataset.zvalimg, width, height, rotationvalue, p0, p1, p, background=background_img) self.progressbar.setValue(i+1) if i>3: @@ -665,15 +690,19 @@ class OpticalScan(QtWidgets.QWidget): ttot = timerunning*Ngrid/(i+1) time2go = ttot - timerunning self.progresstime.setText(self.timelabeltext + str(datetime.timedelta(seconds=round(time2go)))) - self.imageUpdate.emit(self.view.microscopeMode) + # reload image in sampleview, calls loadPixmap + # not needed anymore as the scene gets manipulated directly via self.pyramid + #self.imageUpdate.emit(self.view.microscopeMode) if i==Ngrid-1: - cv2imwrite_fix(self.dataset.getImageName(), cv2.cvtColor(self.view.imgdata, cv2.COLOR_RGB2BGR)) + #cv2imwrite_fix(self.dataset.getImageName(), cv2.cvtColor(self.view.imgdata, cv2.COLOR_RGB2BGR)) self.dataset.saveZvalImg() self.process.join() self.dataqueue.close() self.dataqueue.join_thread() + self.pyramid.toDataset() + if self.deleteImgChecker.isChecked(): path = self.dataset.getScanPath() files = os.listdir(path) diff --git a/sampleview.py b/sampleview.py index 60c2711..cfa2b29 100644 --- a/sampleview.py +++ b/sampleview.py @@ -1,825 +1,859 @@ -# -*- 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 . -""" -from PyQt5 import QtCore, QtGui, QtWidgets -import numpy as np -import os -import cv2 -import time -from .dataset import DataSet, loadData -from .ramancom.ramancontrol import RamanControl, simulatedRaman -from .opticalscan import OpticalScan -from .ramanscanui import RamanScanUI -from .detectionview import ParticleDetectionView -from .analysis.analysisview import ParticleAnalysis -from .zeissimporter import ZeissImporter -from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo, SeedPoint -from .helperfunctions import polygoncovering, cv2imread_fix -from .analysis.colorlegend import getColorFromNameWithSeed -from .analysis.particleEditor import ParticleEditor -from .ramancom.configRaman import RamanConfigWin - -class SampleView(QtWidgets.QGraphicsView): - ScalingChanged = QtCore.pyqtSignal(float) - ParticleOfIndexSelected = QtCore.pyqtSignal(int) - - def __init__(self, logpath): - super(SampleView, self).__init__() - - self.logpath = logpath - self.item = QtWidgets.QGraphicsPixmapItem() - self.item.setPos(0, 0) - self.item.setAcceptedMouseButtons(QtCore.Qt.NoButton) - self.scaleFactor = 1.0 - scene = QtWidgets.QGraphicsScene(self) - scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) - scene.addItem(self.item) - scene.setBackgroundBrush(QtCore.Qt.darkGray) - self.setScene(scene) - self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) - self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) - self.setRenderHint(QtGui.QPainter.Antialiasing) - self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) - self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) - - self.ramanctrl = RamanControl() - self.simulatedRaman = simulatedRaman - self.ramanSwitchNeeded = False - if self.ramanctrl.name == 'RenishawCOM': - #determine, if ramanSwitch is needed: - self.ramanctrl.connect() - if not self.ramanctrl.connected: - QtWidgets.QMessageBox.warning(self, 'Error', 'Please enable Raman Connection') - return - if self.ramanctrl.getImageDimensions(mode='bf')[0] == self.ramanctrl.getImageDimensions(mode='df')[0]: - self.ramanSwitchNeeded = False - else: - self.ramanSwitchNeeded = True - self.ramanctrl.disconnect() - - self.drag = None - self.mode = None - self.dataset = None - self.particleEditor = None - self.fititems = [] - self.boundaryitems = [[],[]] - self.scanitems = [] - self.ramanscanitems = [] - self.particleInfoBox = None - self.imgdata = None - self.isblocked = False - self.contourItems = [] - self.selectedParticleIndices = [] - self.seedPoints = [] - self.particlePainter = None - - self.detectionwidget = None - self.ramanwidget = RamanScanUI(self.ramanctrl, None, self.logpath, self) - self.ramanwidget.imageUpdate.connect(self.loadPixmap) - self.oscanwidget = OpticalScan(self.ramanctrl, None, self.logpath, self) - self.oscanwidget.imageUpdate.connect(self.loadPixmap) - self.oscanwidget.boundaryUpdate.connect(self.resetBoundary) - self.analysiswidget = None - self.setMinimumSize(600, 600) - self.darkenPixmap = False - self.microscopeMode = None - - self.update() - - def takeScreenshot(self): - #TODO: - #LIMIT SCREENSHOT TO ACTUAL VIEWSIZE OF LOADED IMAGE... - #hide scrollbars - self.setHorizontalScrollBarPolicy(1) - self.setVerticalScrollBarPolicy(1) - #capture screen - screen = QtWidgets.QApplication.primaryScreen() - self.repaint() - screenshot = screen.grabWindow(self.winId()) - #unhide scrollbars - self.setHorizontalScrollBarPolicy(0) - self.setVerticalScrollBarPolicy(0) - - fname = self.dataset.path + '/screenshot.png' - validFileName = False - incr = 1 - while not validFileName: - if not os.path.exists(fname): - validFileName = True - else: - fname = self.dataset.path + '/screenshot ({}).png'.format(incr) - incr += 1 - screenshot.save(fname , 'png') - QtWidgets.QMessageBox.about(self, 'Message', 'Saved as {} to project directory.'.format(fname.split('/')[-1])) - - def closeEvent(self, event): - reply = QtWidgets.QMessageBox.question(self, 'Message', - "Do you really want to quit?", QtWidgets.QMessageBox.Yes | - QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) - - if reply == QtWidgets.QMessageBox.Yes: - self.disconnectRaman() - self.saveDataSet() - event.accept() - self.oscanwidget.close() - if self.detectionwidget is not None: - self.detectionwidget.close() - if self.analysiswidget is not None: - self.analysiswidget.close() - self.ramanwidget.close() - else: - event.ignore() - - def configureRamanControl(self): - """ - Launches a window for updating Raman instrument configuration. - :return: - """ - self.configWin = RamanConfigWin(self) - self.configWin.show() - - def saveDataSet(self): - if self.dataset is not None: - self.dataset.save() - - @QtCore.pyqtSlot() - def zoomIn(self): - self.zoomDisplay(1.25) - - @QtCore.pyqtSlot() - def zoomOut(self): - self.zoomDisplay(0.8) - - @QtCore.pyqtSlot() - def normalSize(self): - self.scaleFactor = 1.0 - self.setTransform(QtGui.QTransform.fromScale(1., 1.)) - self.announceScaling() - - @QtCore.pyqtSlot() - def fitToWindow(self): - """ - Fits the window to show the entire sample. - :return: - """ - brect = self.item.sceneBoundingRect() - self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) - self.scaleFactor = self.transform().m11() - self.announceScaling() - - def switchMode(self, mode, loadnew=False): - """ - Switch the gepard to another module - :return: - """ - if mode is None: - return - assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] - print("switching to mode:", mode, flush=True) - self.oscanwidget.setVisible(False) -# if self.detectionwidget is not None: -# self.detectionwidget.close() -# self.detectionwidget = None - self.ramanwidget.setVisible(False) - self.mode = mode - self.loadPixmap(self.microscopeMode) - if mode == "OpticalScan": - self.oscanwidget.setVisible(True) - self.oscanwidget.resetDataset(self.dataset) - - elif mode == "ParticleDetection": - if self.detectionwidget is None: - print('creating new detect window') - self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) - self.detectionwidget.imageUpdate.connect(self.detectionUpdate) - self.detectionwidget.detectionFinished.connect(self.activateMaxMode) - self.updateSeedPointMarkers() - self.detectionwidget.show() - - elif mode == "RamanScan": - self.removeSeedPointMarkers() - self.ramanwidget.resetDataset(self.dataset) - self.ramanwidget.setVisible(True) - - elif mode == "ParticleAnalysis": - self.removeSeedPointMarkers() - if self.ramanwidget.isVisible(): - self.ramanwidget.setVisible(False) - if self.analysiswidget is None: - print('creating new analysiswidget') - self.analysiswidget = ParticleAnalysis(self.dataset, self) - self.analysiswidget.showMaximized() - self.setupParticleEditor() - else: - print('show maximized already exisiting analysiswidget') - self.analysiswidget.showMaximized() - - if self.detectionwidget is not None: - self.detectionwidget.setVisible(False) - - #show legend: - self.imparent.legend.show() - - if loadnew: - self.fitToWindow() - self.imparent.updateModes(mode, self.getMaxMode()) - - def open(self, fname): - self.saveDataSet() - - #close all widgets - for widget in [self.detectionwidget, self.ramanwidget, self.oscanwidget, self.analysiswidget]: - if widget is not None: - widget.close() - widget.destroy() - del widget - - self.dataset = loadData(fname) - self.setupParticleEditor() - self.setMicroscopeMode() - self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) - self.imgdata = None - self.activateMaxMode(loadnew=True) - self.imparent.snapshotAct.setEnabled(True) - - def importProject(self, fname): - zimp = ZeissImporter(fname, self.ramanctrl, self) - if zimp.validimport: - zimp.exec() - if zimp.result() == QtWidgets.QDialog.Accepted: - self.open(zimp.gepardname) - - def new(self, fname): - self.saveDataSet() - if self.dataset is not None: - self.dataset.save() - self.dataset = DataSet(fname, newProject=True) - self.setupParticleEditor() - self.setMicroscopeMode() - self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) - self.imgdata = None - self.activateMaxMode(loadnew=True) - self.imparent.snapshotAct.setEnabled(True) - - def setupParticleEditor(self): - """ - Setting up the particle editor for editing properties or contours of particles. - It needs some connections to sampleview and analysisview. - """ - def tryDisconnectingSignal(signal): - try: - signal.disconnect() - except TypeError: - pass - - if self.particleEditor is None: - self.particleEditor = ParticleEditor(self, self.dataset.particleContainer) - - tryDisconnectingSignal(self.particleEditor.particleAssignmentChanged) - if self.analysiswidget is not None: - self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updatePlotsAndContours) - self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updateWidgetContents) - - def setMicroscopeMode(self): - """ - The opical microscope can be in either Brightfield (bf) or Darkfield (df) mode. In the case of the Renishaw instrument - this mode affects the current image size in µm. Hence, gepard needs to be aware of the current mode - :return: - """ - if self.ramanSwitchNeeded: - self.imparent.ramanSwitch.connectToSampleView() - self.imparent.ramanSwitch.show() - self.microscopeMode = ('df' if self.imparent.ramanSwitch.df_btn.isChecked() else 'bf') - - @QtCore.pyqtSlot() - def activateMaxMode(self, loadnew=False): - """ - Activates the "highest" available mode of gepard. The modes follow gepard's operation workflow: - OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis - :return: - """ - mode = self.getMaxMode() - self.imparent.updateModes(self.mode, self.getMaxMode()) - self.switchMode(mode, loadnew=loadnew) - - def getMaxMode(self): - """ - Determines the "highest" available mode of gepard. The modes follow gepard's operation workflow: - OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis - :return: - """ - if self.dataset is None: - return None - if not self.ramanctrl.connected: - self.connectRaman() - if not self.ramanctrl.connected: - return None - maxmode = "OpticalScan" - if os.path.exists(self.dataset.getImageName()): - maxmode = "ParticleDetection" - if self.dataset.particleDetectionDone: - maxmode = "RamanScan" - if self.dataset.ramanscandone: - maxmode = "ParticleAnalysis" - return maxmode - - def blockUI(self): - self.isblocked = True - self.imparent.blockUI() - - def unblockUI(self): - self.isblocked = False - self.imparent.unblockUI(self.ramanctrl.connected) - self.imparent.updateModes(self.mode, self.getMaxMode()) - - def mousePressEvent(self, event): - if event.button()==QtCore.Qt.MiddleButton: - self.drag = event.pos() - - if self.particlePainter is None: - if event.button()==QtCore.Qt.LeftButton: - if self.analysiswidget is not None: - self.checkForContourSelection(event) - - if self.mode in ["OpticalScan", "RamanScan"] and event.modifiers()==QtCore.Qt.ControlModifier: - p0 = self.mapToScene(event.pos()) - self.moveStageToPosition(p0) - - 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: - p0 = event.pos() - move = self.drag-p0 - self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) - self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) - - self.drag = p0 - elif self.particlePainter is None: - p0 = self.mapToScene(event.pos()) - super(SampleView, self).mouseMoveEvent(event) - else: - self.particlePainter.mouseMoveEvent(event) - - def mouseReleaseEvent(self, event): - self.drag = None - if self.particlePainter is None: - super(SampleView, self).mouseReleaseEvent(event) - else: - self.particlePainter.mouseReleaseEvent(event) - - def wheelEvent(self, event): - if self.particlePainter is None: - factor = 1.01**(event.angleDelta().y()/8) - self.zoomDisplay(factor) - else: - self.particlePainter.wheelEvent(event) - - def keyPressEvent(self, event): - if self.particlePainter is not None: - self.particlePainter.keyPressEvent(event) - - def moveStageToPosition(self, pos): - """ - Sends a command to the ramancontrol to move the microscope stage to the position of the click event. - :return: - """ - if self.dataset is not None and self.dataset.pshift is not None: - if self.dataset.readin: - reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!', - "Coordinate systems might have changed since. Do you want to continue with saved coordinates?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) - - if reply == QtWidgets.QMessageBox.Yes: - self.dataset.readin = False - else: - return - - noz = (self.mode in ['OpticalScan', 'RamanScan']) - x, y, z = self.dataset.mapToLengthRaman([pos.x(), pos.y()], microscopeMode=self.microscopeMode, noz=noz) - if z is not None: - assert z>-100. - self.ramanctrl.moveToAbsolutePosition(x, y, z) - - def checkForContourSelection(self, event): - """ - Checks, if at the given event position any contour is selected. - :return: - """ - def acceptSelection(cnt): - self.ParticleOfIndexSelected.emit(cnt.particleIndex) - cnt.isSelected = True - cnt.update() - if cnt.particleIndex not in self.selectedParticleIndices: - self.selectedParticleIndices.append(cnt.particleIndex) - - def removeContourFromSelection(cnt): - cnt.isSelected = False - cnt.update() - self.selectedParticleIndices.remove(cnt.particleIndex) - - p = self.mapToScene(event.pos()) - p = QtCore.QPointF(p.x(), p.y()) - - for index, cnt in enumerate(self.contourItems): - - if cnt.polygon.containsPoint(p, QtCore.Qt.OddEvenFill): #clicked on particle - if not event.modifiers()==QtCore.Qt.ShiftModifier: - acceptSelection(cnt) - else: - if cnt.particleIndex not in self.selectedParticleIndices: - acceptSelection(cnt) - elif cnt.particleIndex in self.selectedParticleIndices: - removeContourFromSelection(cnt) - - else: #not clicked on particle - if event.modifiers()!=QtCore.Qt.ShiftModifier: - cnt.isSelected = False - cnt.update() - if cnt.particleIndex in self.selectedParticleIndices: - self.selectedParticleIndices.remove(cnt.particleIndex) - - if len(self.selectedParticleIndices) > 0: - self.updateParticleInfoBox(self.selectedParticleIndices[-1]) - else: - self.removeParticleInfoBox() - - self.update() - - def zoomDisplay(self, factor): - """ - Zooms the GraphicsView in or out, according to the given factor. - :return: - """ - if factor<1 and not self.imparent.zoomOutAct.isEnabled(): - return - if factor>1 and not self.imparent.zoomInAct.isEnabled(): - return - self.scaleFactor *= factor - self.scale(factor, factor) - self.announceScaling() - - def announceScaling(self): - """ - Processes a new scaling of the GraphicsView. - :return: - """ - pixelscale = self.dataset.getPixelScale(self.microscopeMode) - if self.dataset is None or pixelscale is None: - self.ScalingChanged.emit(-1.0) - else: - self.ScalingChanged.emit(pixelscale/self.scaleFactor) ##CURRENTLY ONLY DARKFIELD!!! FIX NEEDED!!! - - def connectRaman(self): - if not self.ramanctrl.connect(): - msg = QtWidgets.QMessageBox() - msg.setText("Connection failed! Please enable remote control.") - msg.exec() - else: - mode = self.getMaxMode() - self.switchMode(mode) - self.imparent.updateConnected(self.ramanctrl.connected) - - def disconnectRaman(self): - self.ramanctrl.disconnect() - self.imparent.updateConnected(self.ramanctrl.connected) - - @QtCore.pyqtSlot(str) - def detectionUpdate(self): - """ - Is connected to particle detection. When a new segmentation result was obtained, the sampleview has to update here. - :return: - """ - self.resetParticleContours() - self.prepareAnalysis() - self.update() - - @QtCore.pyqtSlot(str) - def loadPixmap(self, microscope_mode='df'): - """ - Loads the pixmap image as background for the GraphicsView. - It is stored in self.imgdata - :return: - """ - self.clearItems() - if self.dataset is None: - self.item.setPixmap(QtGui.QPixmap()) - else: - data = self.imgdata - fname = self.dataset.getImageName() - if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": - self.resetParticleContours() - if data is None and os.path.exists(fname): - data = cv2imread_fix(fname) - self.imgdata = data - if data is not None: - height, width, channel = data.shape - bytesPerLine = 3 * width - pix = QtGui.QPixmap() - pix.convertFromImage(QtGui.QImage(data.data, - width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) - self.item.setPixmap(pix) - - if self.darkenPixmap: - self.scene().setBackgroundBrush(QtGui.QColor(5, 5, 5)) - self.item.setOpacity(0.2) - else: - self.scene().setBackgroundBrush(QtCore.Qt.darkGray) - self.item.setOpacity(1) - - else: - self.item.setPixmap(QtGui.QPixmap()) - if self.mode == "OpticalScan": - for i, p in zip(self.dataset.fitindices, self.dataset.fitpoints): - p = self.dataset.mapToPixel(p, mode=microscope_mode, force=True) - fititem = FitPosIndicator(i+1, pos=p) - self.scene().addItem(fititem) - self.fititems.append(fititem) - if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": - self.prepareAnalysis() - else: - self.fitToWindow() - - def prepareAnalysis(self): - """ - Is called after final particle detection. Raman Scan Indicators are added. - """ - self.clearItems() - if self.dataset.particleDetectionDone: - for meas in self.dataset.particleContainer.measurements: - if meas.ramanScanIndex is not None: - number = meas.ramanScanIndex+1 - item = RamanScanIndicator(self, number, 20, (meas.pixelcoord_x, meas.pixelcoord_y)) - self.scene().addItem(item) - self.ramanscanitems.append(item) - - def clearItems(self): - """ - Removes items from the GraphicsScene. ContourItems remain, however.. - :return: - """ - for item in self.fititems: - self.scene().removeItem(item) - self.fititems = [] - - for item in self.scanitems: - self.scene().removeItem(item) - self.scanitems = [] - - for item in self.ramanscanitems: - self.scene().removeItem(item) - self.ramanscanitems = [] - - edges, nodes = self.boundaryitems - for item in edges: - self.scene().removeItem(item) - for item in nodes: - self.scene().removeItem(item) - self.boundaryitems = [], [] - - @QtCore.pyqtSlot() - def resetScanPositions(self): - """ - Calculates position of scan tiles for the optical scan, according to the boundary and the polygoncovering method. - The corresponding graphic items are added to the scene. - :return: - """ - micMode = ('df' if self.oscanwidget.df_btn.isChecked() else 'bf') - for item in self.scanitems: - self.scene().removeItem(item) - edges, nodes = self.boundaryitems - boundary = [] - for n in nodes: - p = n.pos().x(), n.pos().y() - boundary.append(self.dataset.mapToLength(p, self.microscopeMode, force=True)) - boundary = np.array(boundary) - self.dataset.boundary = boundary - if micMode == 'df': - width, height, angle = self.dataset.imagedim_df - else: - width, height, angle = self.dataset.imagedim_bf - - margin = min(width, height)*0.02 - wx, wy = width-margin, height-margin - print(wx,wy) - p1 = polygoncovering(boundary, wx, wy) - b2 = boundary.copy() - b2 = b2[:,[1,0]] - p2 = polygoncovering(b2, wy, wx) - if len(p2) + +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 . +""" +from PyQt5 import QtCore, QtGui, QtWidgets +import numpy as np +import os +import cv2 +import time +from .dataset import DataSet, loadData +from .ramancom.ramancontrol import RamanControl, simulatedRaman +from .opticalscan import OpticalScan +from .ramanscanui import RamanScanUI +from .detectionview import ParticleDetectionView +from .analysis.analysisview import ParticleAnalysis +from .zeissimporter import ZeissImporter +from .viewitems import FitPosIndicator, Node, Edge, ScanIndicator, RamanScanIndicator, SegmentationContour, ParticleInfo, SeedPoint +from .helperfunctions import polygoncovering, cv2imread_fix +from .analysis.colorlegend import getColorFromNameWithSeed +from .analysis.particleEditor import ParticleEditor +from .ramancom.configRaman import RamanConfigWin +from .scenePyramid import ScenePyramid + +class SampleView(QtWidgets.QGraphicsView): + ScalingChanged = QtCore.pyqtSignal(float) + ParticleOfIndexSelected = QtCore.pyqtSignal(int) + + def __init__(self, logpath): + super(SampleView, self).__init__() + + self.logpath = logpath + #self.item = QtWidgets.QGraphicsPixmapItem() + #self.item.setPos(0, 0) + #self.item.setAcceptedMouseButtons(QtCore.Qt.NoButton) + self.scaleFactor = 1.0 + scene = QtWidgets.QGraphicsScene(self) + scene.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex) + #scene.addItem(self.item) + scene.setBackgroundBrush(QtCore.Qt.darkGray) + self.setScene(scene) + self.setCacheMode(QtWidgets.QGraphicsView.CacheBackground) + self.setViewportUpdateMode(QtWidgets.QGraphicsView.BoundingRectViewportUpdate) + self.setRenderHint(QtGui.QPainter.Antialiasing) + self.setTransformationAnchor(QtWidgets.QGraphicsView.AnchorUnderMouse) + self.setResizeAnchor(QtWidgets.QGraphicsView.AnchorViewCenter) + + self.ramanctrl = RamanControl() + self.simulatedRaman = simulatedRaman + self.ramanSwitchNeeded = False + if self.ramanctrl.name == 'RenishawCOM': + #determine, if ramanSwitch is needed: + self.ramanctrl.connect() + if not self.ramanctrl.connected: + QtWidgets.QMessageBox.warning(self, 'Error', 'Please enable Raman Connection') + return + if self.ramanctrl.getImageDimensions(mode='bf')[0] == self.ramanctrl.getImageDimensions(mode='df')[0]: + self.ramanSwitchNeeded = False + else: + self.ramanSwitchNeeded = True + self.ramanctrl.disconnect() + + self.drag = None + self.mode = None + self.dataset = None + self.pyramid = ScenePyramid(self) + self.particleEditor = None + self.fititems = [] + self.boundaryitems = [[],[]] + self.scanitems = [] + self.ramanscanitems = [] + self.particleInfoBox = None + self.imgdata = None + self.isblocked = False + self.contourItems = [] + self.selectedParticleIndices = [] + self.seedPoints = [] + self.particlePainter = None + + self.detectionwidget = None + self.ramanwidget = RamanScanUI(self.ramanctrl, None, self.logpath, self) + self.ramanwidget.imageUpdate.connect(self.loadPixmap) + self.oscanwidget = OpticalScan(self.ramanctrl, None, self.logpath, self) + self.oscanwidget.imageUpdate.connect(self.loadPixmap) + self.oscanwidget.boundaryUpdate.connect(self.resetBoundary) + self.analysiswidget = None + self.setMinimumSize(600, 600) + self.darkenPixmap = False + self.microscopeMode = None + + self.update() + + def takeScreenshot(self): + #TODO: + #LIMIT SCREENSHOT TO ACTUAL VIEWSIZE OF LOADED IMAGE... + #hide scrollbars + self.setHorizontalScrollBarPolicy(1) + self.setVerticalScrollBarPolicy(1) + #capture screen + screen = QtWidgets.QApplication.primaryScreen() + self.repaint() + screenshot = screen.grabWindow(self.winId()) + #unhide scrollbars + self.setHorizontalScrollBarPolicy(0) + self.setVerticalScrollBarPolicy(0) + + fname = self.dataset.path + '/screenshot.png' + validFileName = False + incr = 1 + while not validFileName: + if not os.path.exists(fname): + validFileName = True + else: + fname = self.dataset.path + '/screenshot ({}).png'.format(incr) + incr += 1 + screenshot.save(fname , 'png') + QtWidgets.QMessageBox.about(self, 'Message', 'Saved as {} to project directory.'.format(fname.split('/')[-1])) + + def closeEvent(self, event): + reply = QtWidgets.QMessageBox.question(self, 'Message', + "Do you really want to quit?", QtWidgets.QMessageBox.Yes | + QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + + if reply == QtWidgets.QMessageBox.Yes: + self.disconnectRaman() + self.saveDataSet() + event.accept() + self.oscanwidget.close() + if self.detectionwidget is not None: + self.detectionwidget.close() + if self.analysiswidget is not None: + self.analysiswidget.close() + self.ramanwidget.close() + else: + event.ignore() + + def configureRamanControl(self): + """ + Launches a window for updating Raman instrument configuration. + :return: + """ + self.configWin = RamanConfigWin(self) + self.configWin.show() + + def saveDataSet(self): + if self.dataset is not None: + self.dataset.save() + + def scrollContentsBy(self, dx: int, dy: int) -> None: + super().scrollContentsBy(dx, dy) + self.pyramid.onMove() + + @QtCore.pyqtSlot() + def zoomIn(self): + self.zoomDisplay(1.25) + + @QtCore.pyqtSlot() + def zoomOut(self): + self.zoomDisplay(0.8) + + @QtCore.pyqtSlot() + def normalSize(self): + self.scaleFactor = 1.0 + self.setTransform(QtGui.QTransform.fromScale(1., 1.)) + self.announceScaling() + + @QtCore.pyqtSlot() + def fitToWindow(self): + """ + Fits the window to show the entire sample. + :return: + """ + brect = self.scene().itemsBoundingRect() + #brect = self.item.sceneBoundingRect() + self.fitInView(0, 0, brect.width(), brect.height(), QtCore.Qt.KeepAspectRatio) + self.scaleFactor = self.transform().m11() + self.announceScaling() + + def switchMode(self, mode, loadnew=False): + """ + Switch the gepard to another module + :return: + """ + if mode is None: + return + assert mode in ["OpticalScan", "ParticleDetection", "RamanScan", "ParticleAnalysis"] + print("switching to mode:", mode, flush=True) + self.oscanwidget.setVisible(False) +# if self.detectionwidget is not None: +# self.detectionwidget.close() +# self.detectionwidget = None + self.ramanwidget.setVisible(False) + self.mode = mode + self.loadPixmap(self.microscopeMode) + if mode == "OpticalScan": + self.oscanwidget.setVisible(True) + self.oscanwidget.resetDataset(self.dataset) + self.oscanwidget.setPyramid(self.pyramid) + + elif mode == "ParticleDetection": + if self.detectionwidget is None: + print('creating new detect window') + self.detectionwidget = ParticleDetectionView( + self.pyramid, + self.dataset, + self + ) + #self.detectionwidget = ParticleDetectionView(self.imgdata, self.dataset, self) + self.detectionwidget.imageUpdate.connect(self.detectionUpdate) + self.detectionwidget.detectionFinished.connect(self.activateMaxMode) + self.updateSeedPointMarkers() + self.detectionwidget.show() + + elif mode == "RamanScan": + self.removeSeedPointMarkers() + self.ramanwidget.resetDataset(self.dataset) + self.ramanwidget.setVisible(True) + + elif mode == "ParticleAnalysis": + self.removeSeedPointMarkers() + if self.ramanwidget.isVisible(): + self.ramanwidget.setVisible(False) + if self.analysiswidget is None: + print('creating new analysiswidget') + self.analysiswidget = ParticleAnalysis(self.dataset, self) + self.analysiswidget.showMaximized() + self.setupParticleEditor() + else: + print('show maximized already exisiting analysiswidget') + self.analysiswidget.showMaximized() + + if self.detectionwidget is not None: + self.detectionwidget.setVisible(False) + + #show legend: + self.imparent.legend.show() + + if loadnew: + self.fitToWindow() + self.imparent.updateModes(mode, self.getMaxMode()) + + def open(self, fname): + self.saveDataSet() + + #close all widgets + for widget in [self.detectionwidget, self.ramanwidget, self.oscanwidget, self.analysiswidget]: + if widget is not None: + widget.close() + widget.destroy() + del widget + + self.dataset = loadData(fname) + self.setupParticleEditor() + self.setMicroscopeMode() + + self.pyramid.fromDataset(self.dataset) + self.pyramid.setMicroscopeMode(self.microscopeMode) + + self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) + self.imgdata = None + self.activateMaxMode(loadnew=True) + self.imparent.snapshotAct.setEnabled(True) + + def importProject(self, fname): + zimp = ZeissImporter(fname, self.ramanctrl, self) + if zimp.validimport: + zimp.exec() + if zimp.result() == QtWidgets.QDialog.Accepted: + self.open(zimp.gepardname) + + def new(self, fname): + self.saveDataSet() + if self.dataset is not None: + self.dataset.save() + self.dataset = DataSet(fname, newProject=True) + self.setupParticleEditor() + self.setMicroscopeMode() + self.pyramid.fromDataset(self.dataset) + self.pyramid.setMicroscopeMode(self.microscopeMode) + self.imparent.setWindowTitle(self.dataset.name + (" SIMULATION" if simulatedRaman else "")) + self.imgdata = None + self.activateMaxMode(loadnew=True) + self.imparent.snapshotAct.setEnabled(True) + + def setupParticleEditor(self): + """ + Setting up the particle editor for editing properties or contours of particles. + It needs some connections to sampleview and analysisview. + """ + def tryDisconnectingSignal(signal): + try: + signal.disconnect() + except TypeError: + pass + + if self.particleEditor is None: + self.particleEditor = ParticleEditor(self, self.dataset.particleContainer) + + tryDisconnectingSignal(self.particleEditor.particleAssignmentChanged) + if self.analysiswidget is not None: + self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updatePlotsAndContours) + self.particleEditor.particleAssignmentChanged.connect(self.analysiswidget.updateWidgetContents) + + def setMicroscopeMode(self): + """ + The opical microscope can be in either Brightfield (bf) or Darkfield (df) mode. In the case of the Renishaw instrument + this mode affects the current image size in µm. Hence, gepard needs to be aware of the current mode + :return: + """ + if self.ramanSwitchNeeded: + self.imparent.ramanSwitch.connectToSampleView() + self.imparent.ramanSwitch.show() + self.microscopeMode = ('df' if self.imparent.ramanSwitch.df_btn.isChecked() else 'bf') + + @QtCore.pyqtSlot() + def activateMaxMode(self, loadnew=False): + """ + Activates the "highest" available mode of gepard. The modes follow gepard's operation workflow: + OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis + :return: + """ + mode = self.getMaxMode() + self.imparent.updateModes(self.mode, self.getMaxMode()) + self.switchMode(mode, loadnew=loadnew) + + def getMaxMode(self): + """ + Determines the "highest" available mode of gepard. The modes follow gepard's operation workflow: + OpticalScan, ParticleDetection, RamanScan, ParticleAnalysis + :return: + """ + if self.dataset is None: + return None + if not self.ramanctrl.connected: + self.connectRaman() + if not self.ramanctrl.connected: + return None + maxmode = "OpticalScan" + if os.path.exists(self.dataset.getImageName()): + maxmode = "ParticleDetection" + if self.dataset.particleDetectionDone: + maxmode = "RamanScan" + if self.dataset.ramanscandone: + maxmode = "ParticleAnalysis" + return maxmode + + def blockUI(self): + self.isblocked = True + self.imparent.blockUI() + + def unblockUI(self): + self.isblocked = False + self.imparent.unblockUI(self.ramanctrl.connected) + self.imparent.updateModes(self.mode, self.getMaxMode()) + + def mousePressEvent(self, event): + if event.button()==QtCore.Qt.MiddleButton: + self.drag = event.pos() + + if self.particlePainter is None: + if event.button()==QtCore.Qt.LeftButton: + if self.analysiswidget is not None: + self.checkForContourSelection(event) + + if self.mode in ["OpticalScan", "RamanScan"] and event.modifiers()==QtCore.Qt.ControlModifier: + p0 = self.mapToScene(event.pos()) + self.moveStageToPosition(p0) + + 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: + p0 = event.pos() + move = self.drag-p0 + self.horizontalScrollBar().setValue(move.x() + self.horizontalScrollBar().value()) + self.verticalScrollBar().setValue(move.y() + self.verticalScrollBar().value()) + + self.drag = p0 + elif self.particlePainter is None: + p0 = self.mapToScene(event.pos()) + super(SampleView, self).mouseMoveEvent(event) + else: + self.particlePainter.mouseMoveEvent(event) + + def mouseReleaseEvent(self, event): + self.drag = None + if self.particlePainter is None: + super(SampleView, self).mouseReleaseEvent(event) + else: + self.particlePainter.mouseReleaseEvent(event) + + def wheelEvent(self, event): + if self.particlePainter is None: + factor = 1.01**(event.angleDelta().y()/8) + self.zoomDisplay(factor) + else: + self.particlePainter.wheelEvent(event) + + def keyPressEvent(self, event): + if self.particlePainter is not None: + self.particlePainter.keyPressEvent(event) + + def moveStageToPosition(self, pos): + """ + Sends a command to the ramancontrol to move the microscope stage to the position of the click event. + :return: + """ + if self.dataset is not None and self.dataset.pshift is not None: + if self.dataset.readin: + reply = QtWidgets.QMessageBox.critical(self, 'Dataset is newly read from disk!', + "Coordinate systems might have changed since. Do you want to continue with saved coordinates?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, QtWidgets.QMessageBox.No) + + if reply == QtWidgets.QMessageBox.Yes: + self.dataset.readin = False + else: + return + + noz = (self.mode in ['OpticalScan', 'RamanScan']) + x, y, z = self.dataset.mapToLengthRaman([pos.x(), pos.y()], microscopeMode=self.microscopeMode, noz=noz) + if z is not None: + assert z>-100. + self.ramanctrl.moveToAbsolutePosition(x, y, z) + + def checkForContourSelection(self, event): + """ + Checks, if at the given event position any contour is selected. + :return: + """ + def acceptSelection(cnt): + self.ParticleOfIndexSelected.emit(cnt.particleIndex) + cnt.isSelected = True + cnt.update() + if cnt.particleIndex not in self.selectedParticleIndices: + self.selectedParticleIndices.append(cnt.particleIndex) + + def removeContourFromSelection(cnt): + cnt.isSelected = False + cnt.update() + self.selectedParticleIndices.remove(cnt.particleIndex) + + p = self.mapToScene(event.pos()) + p = QtCore.QPointF(p.x(), p.y()) + + for index, cnt in enumerate(self.contourItems): + + if cnt.polygon.containsPoint(p, QtCore.Qt.OddEvenFill): #clicked on particle + if not event.modifiers()==QtCore.Qt.ShiftModifier: + acceptSelection(cnt) + else: + if cnt.particleIndex not in self.selectedParticleIndices: + acceptSelection(cnt) + elif cnt.particleIndex in self.selectedParticleIndices: + removeContourFromSelection(cnt) + + else: #not clicked on particle + if event.modifiers()!=QtCore.Qt.ShiftModifier: + cnt.isSelected = False + cnt.update() + if cnt.particleIndex in self.selectedParticleIndices: + self.selectedParticleIndices.remove(cnt.particleIndex) + + if len(self.selectedParticleIndices) > 0: + self.updateParticleInfoBox(self.selectedParticleIndices[-1]) + else: + self.removeParticleInfoBox() + + self.update() + + def zoomDisplay(self, factor): + """ + Zooms the GraphicsView in or out, according to the given factor. + :return: + """ + if factor<1 and not self.imparent.zoomOutAct.isEnabled(): + return + if factor>1 and not self.imparent.zoomInAct.isEnabled(): + return + self.scaleFactor *= factor + self.scale(factor, factor) + self.pyramid.onScale() + self.announceScaling() + + def announceScaling(self): + """ + Processes a new scaling of the GraphicsView. + :return: + """ + pixelscale = self.dataset.getPixelScale(self.microscopeMode) + if self.dataset is None or pixelscale is None: + self.ScalingChanged.emit(-1.0) + else: + self.ScalingChanged.emit(pixelscale/self.scaleFactor) ##CURRENTLY ONLY DARKFIELD!!! FIX NEEDED!!! + + def connectRaman(self): + if not self.ramanctrl.connect(): + msg = QtWidgets.QMessageBox() + msg.setText("Connection failed! Please enable remote control.") + msg.exec() + else: + mode = self.getMaxMode() + self.switchMode(mode) + self.imparent.updateConnected(self.ramanctrl.connected) + + def disconnectRaman(self): + self.ramanctrl.disconnect() + self.imparent.updateConnected(self.ramanctrl.connected) + + @QtCore.pyqtSlot(str) + def detectionUpdate(self): + """ + Is connected to particle detection. When a new segmentation result was obtained, the sampleview has to update here. + :return: + """ + self.resetParticleContours() + self.prepareAnalysis() + self.update() + + def addEmptyItem(self): + item = QtWidgets.QGraphicsPixmapItem() + item.setPos(0, 0) + item.setAcceptedMouseButtons(QtCore.Qt.NoButton) + item.setPixmap(QtGui.QPixmap()) + self.scene().addItem(item) + + @QtCore.pyqtSlot(str) + def loadPixmap(self, microscope_mode='df'): + """ + Loads the pixmap image as background for the GraphicsView. + It is stored in self.imgdata + :return: + """ + self.clearItems() + if self.dataset is None: + #self.item.setPixmap(QtGui.QPixmap()) + None + else: + #data = self.imgdata + #fname = self.dataset.getImageName() + if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": + self.resetParticleContours() + #if data is None and os.path.exists(fname): + # data = cv2imread_fix(fname) + # self.imgdata = data + #if data is not None: + # height, width, channel = data.shape + # bytesPerLine = 3 * width + # pix = QtGui.QPixmap() + # pix.convertFromImage(QtGui.QImage(data.data, + # width, height, bytesPerLine, QtGui.QImage.Format_RGB888)) + # self.item.setPixmap(pix) + + if self.darkenPixmap: + self.scene().setBackgroundBrush(QtGui.QColor(5, 5, 5)) + #self.item.setOpacity(0.2) + self.pyramid.setTileOpacity(0.2) + else: + self.scene().setBackgroundBrush(QtCore.Qt.darkGray) + #self.item.setOpacity(1) + self.pyramid.setTileOpacity(1) + + #else: + # self.item.setPixmap(QtGui.QPixmap()) + + if self.mode == "OpticalScan": + for i, p in zip(self.dataset.fitindices, self.dataset.fitpoints): + p = self.dataset.mapToPixel(p, mode=microscope_mode, force=True) + fititem = FitPosIndicator(i+1, pos=p) + self.scene().addItem(fititem) + self.fititems.append(fititem) + + self.pyramid.initScene() + + if self.mode == "ParticleDetection" or self.mode == "ParticleAnalysis": + self.prepareAnalysis() + else: + self.fitToWindow() + + def prepareAnalysis(self): + """ + Is called after final particle detection. Raman Scan Indicators are added. + """ + self.clearItems() + if self.dataset.particleDetectionDone: + for meas in self.dataset.particleContainer.measurements: + if meas.ramanScanIndex is not None: + number = meas.ramanScanIndex+1 + item = RamanScanIndicator(self, number, 20, (meas.pixelcoord_x, meas.pixelcoord_y)) + self.scene().addItem(item) + self.ramanscanitems.append(item) + + def clearItems(self): + """ + Removes items from the GraphicsScene. ContourItems remain, however.. + :return: + """ + for item in self.fititems: + self.scene().removeItem(item) + self.fititems = [] + + for item in self.scanitems: + self.scene().removeItem(item) + self.scanitems = [] + + for item in self.ramanscanitems: + self.scene().removeItem(item) + self.ramanscanitems = [] + + edges, nodes = self.boundaryitems + for item in edges: + self.scene().removeItem(item) + for item in nodes: + self.scene().removeItem(item) + self.boundaryitems = [], [] + + @QtCore.pyqtSlot() + def resetScanPositions(self): + """ + Calculates position of scan tiles for the optical scan, according to the boundary and the polygoncovering method. + The corresponding graphic items are added to the scene. + :return: + """ + micMode = ('df' if self.oscanwidget.df_btn.isChecked() else 'bf') + for item in self.scanitems: + self.scene().removeItem(item) + edges, nodes = self.boundaryitems + boundary = [] + for n in nodes: + p = n.pos().x(), n.pos().y() + boundary.append(self.dataset.mapToLength(p, self.microscopeMode, force=True)) + boundary = np.array(boundary) + self.dataset.boundary = boundary + if micMode == 'df': + width, height, angle = self.dataset.imagedim_df + else: + width, height, angle = self.dataset.imagedim_bf + + margin = min(width, height)*0.02 + wx, wy = width-margin, height-margin + print(wx,wy) + p1 = polygoncovering(boundary, wx, wy) + b2 = boundary.copy() + b2 = b2[:,[1,0]] + p2 = polygoncovering(b2, wy, wx) + if len(p2) + +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 warnings + +import cv2 +import math +import copy +import numpy as np +from .helperfunctions import cv2imread_fix, cv2imwrite_fix +from PIL import Image +from PyQt5 import QtCore, QtGui, QtWidgets + + +# from .dataset import DataSet + + +class ScenePyramid: + @staticmethod + def getDefaults(): + """ + default values for all params, that get saved in dataset + :return: + """ + return { + 'preScanModeComplete': False, + 'maxSliceNumber': 0, + 'tileDim': (1000, 1000), + 'scalingFactor': .5, + 'tileSets': {}, + 'fullImageWidth': 0, + 'fullImageHeight': 0 + } + + def __init__(self, view: QtWidgets.QGraphicsView): + # references to other objects + self.view: QtWidgets.QGraphicsView = view + self.scene = self.view.scene() + self.dataset = None + + # + self.opacity = 1 + self.xIdx = 0 + self.yIdx = 1 + self.currentSlice = None + self.microscopeMode = None + self.currentTiles = None + self.tileWorkingSets = None + + self.preScanModeComplete = None + self.maxSliceNumber = None + self.tileDim = None + self.scalingFactor = None + self.tileSets = None + self.fullImageWidth = None + self.fullImageHeight = None + + # params to be saved to dataset + self.datasetParams = [ + 'preScanModeComplete', + 'maxSliceNumber', + 'tileDim', + 'scalingFactor', + 'tileSets', + 'fullImageWidth', + 'fullImageHeight' + ] + + self.initDefaults() + + def initDefaults(self): + p = ScenePyramid.getDefaults() + for i in p: + setattr(self, i, p[i]) + + self.currentSlice = 0 + self.microscopeMode = None + self.currentTiles = [] + self.tileWorkingSets = {} + + def onScale(self): + """ + gets called on zoom event in QGraphicsView + :return: + """ + if self.preScanModeComplete: + self.updateScene() + + def onMove(self): + """ + gets called on scroll event in QGraphicsView + :return: + """ + if self.preScanModeComplete: + self.updateScene() + + def initScene(self): + if self.preScanModeComplete: + self.updateScene() + + def updateScene(self): + """ + gets called on move and scale of scene (scrolling and zooming) + + first determines pyramid slice to use so that + width of slice >= self.view.mapFromScene(x, 0) - self.view.mapFromScene(0, 0) + then, checks visibility of all view tiles and removes all that are invisible + and renders those, that are visible + :return: + """ + current_slice = self.currentSlice + self.determineSlice() + + slice_changed = current_slice != self.currentSlice + + if slice_changed: + self.resetScene() + + # map viewport to scene + view_origin = self.view.mapToScene(0, 0) + view_max_x_y = self.view.mapToScene(self.view.width(), self.view.height()) + + # get scene origin (given in microscope coords) in pixel coords + p = self.dataset.maxdim[:2] + scene_origin_x, scene_origin_y = (0, 0) # self.dataset.mapToPixel(p, self.microscopeMode, force=True) + + # default width, height of view tiles + view_tile_width, view_tile_height = self.tileDim + + inv_scaling = (1 / self.scalingFactor) ** self.currentSlice + + if self.currentSlice in self.tileWorkingSets: + for i in self.tileWorkingSets[self.currentSlice]: + for j in self.tileWorkingSets[self.currentSlice][i]: + # check visibility of view tile + tile_width, tile_height = self.getTileDimensions(i, j) + + if tile_width is not False and tile_height is not False: + # get scene coords of tile + tile_origin_x = max(0, i * view_tile_width) + scene_origin_x + tile_origin_y = max(0, j * view_tile_height) + scene_origin_y + + # view tile is of constant size, but will be scaled according to slice number (scaling) + # so its appearent size in the scene will be bigger + # and it will appear at a different position + t_o_x = max(0, i * inv_scaling * view_tile_width) + scene_origin_x + t_o_y = max(0, j * inv_scaling * view_tile_height) + scene_origin_y + + # if tile intersects with viewport + if QtCore.QRectF( + t_o_x, + t_o_y, + tile_width * inv_scaling, + tile_height * inv_scaling + ).intersects( + QtCore.QRectF( + view_origin.x(), + view_origin.y(), + view_max_x_y.x() - view_origin.x(), + view_max_x_y.y() - view_origin.y() + ) + ): + # show/reload tile + self.renderTile(i, j, tile_origin_x, tile_origin_y) + else: + # check if tile needs to be removed + self.removeTile(i, j) + + self.setTileOpacity(self.opacity) + + def determineSlice(self): + """ + calculates current slice by using the view to scene ratio of the the image + :return: + """ + if 0 == self.fullImageWidth: + self.currentSlice = 0 + + # tiles get added to the (theoretically infinite) scene which can be zoomed and translated + # full image width and height are width and height in scene coordinates + # we only see parts of the scene by using a view + + # gets the position of left upper tile origin (from scene) in view coordinates + origin = self.view.mapFromScene(QtCore.QPoint(0, 0)) + # gets the position of lower right tile (from scene) in view coordinates + max_x_y = self.view.mapFromScene(QtCore.QPoint(self.fullImageWidth, self.fullImageHeight)) + # current view size of all tiles + cur_width = max_x_y.x() - origin.x() + + ''' + with f = scaling factor + s = slice number + w = full image width + w' = image width of slice + a slices width can be calculated by + w' = f^s * w + the current slice can be calculated by + s = floor( log( w/w', f ) ) + with accounting for lowest = 0 and highest slice number = s_max we get + s = min( s_max, max( 0, floor( log( w/w', f ))) + ''' + self.currentSlice = min( + self.maxSliceNumber, + max( + 0, + math.floor( + math.log( + cur_width / self.fullImageWidth, + self.scalingFactor + ) + ) + ) + ) + + def updateSceneAfterViewTileUpdate(self, scan_tile_pos): + """ + gets called by self.addSrcTile after adding a new scan tile, to update all view tiles, that may have changed + up to 4 tiles may have to be updated. and because we are lazy we update all 4 every time + after determining the view tile (i, j) that holds the origin of the last added scan tile, we update + (i, J ), (i + 1, j ) + (i, j + 1), (i + 1, j + 1) + :param scan_tile_pos: + :return: + """ + self.determineSlice() + + cur_scaling = self.scalingFactor ** self.currentSlice + inv_scaling = 1 / cur_scaling + cur_full_image_width = cur_scaling * self.fullImageWidth + cur_full_image_height = cur_scaling * self.fullImageHeight + + # get scene origin (given in microscope coords) in pixel coords + p = self.dataset.maxdim[:2] + scene_origin_x, scene_origin_y = (0, 0) # self.dataset.mapToPixel(p, self.microscopeMode, force=True) + + # scaled position of currently added scan tile + pos_x = cur_scaling * scan_tile_pos[self.xIdx] + pos_y = cur_scaling * scan_tile_pos[self.yIdx] + + # default width, height of view tiles + view_tile_width, view_tile_height = self.tileDim + + # indices of the tile, the src tiles origin is located in + i = math.floor(pos_x / view_tile_width) + j = math.floor(pos_y / view_tile_height) + + # max tile count in either direction on current scaling + max_tiles_x = math.ceil(cur_full_image_width / view_tile_width) + max_tiles_y = math.ceil(cur_full_image_height / view_tile_height) + + # map viewport to scene + view_origin = self.view.mapToScene(0, 0) + view_max_x_y = self.view.mapToScene(self.view.width(), self.view.height()) + + for t_i in [0, 1]: + for t_j in [0, 1]: + i_cur = i + t_i + j_cur = j + t_j + if i_cur <= max_tiles_x and j_cur <= max_tiles_y: + # determine visibility of tile (i_cur, j_cur) + # get true width and height of tile (i_cur, j_cur) + tile_width, tile_height = self.getTileDimensions(i_cur, j_cur) + + if tile_width is not False and tile_height is not False: + # get scene coords of tile + tile_origin_x = max(0, i_cur * view_tile_width) + scene_origin_x + tile_origin_y = max(0, j_cur * view_tile_height) + scene_origin_y + + # view tile is of constant size, but will be scaled according to slice number (scaling) + # so its appearent size in the scene will be bigger + # and it will appear at a different position + t_o_x = max(0, i_cur * inv_scaling * view_tile_width) + scene_origin_x + t_o_y = max(0, j_cur * inv_scaling * view_tile_height) + scene_origin_y + + # if tile intersects with viewport + if QtCore.QRectF( + t_o_x, + t_o_y, + tile_width * inv_scaling, + tile_height * inv_scaling + ).intersects( + QtCore.QRectF( + view_origin.x(), + view_origin.y(), + view_max_x_y.x() - view_origin.x(), + view_max_x_y.y() - view_origin.y() + ) + ): + # show/reload tile + self.renderTile(i_cur, j_cur, tile_origin_x, tile_origin_y, force=True) + + def getTile(self, i, j, use_slice=None): + """ + returns tile (i, j) of current (self.currentSlice) or given slice. + tile (i, j) contains the dimensions of the corresponding tile image + and a reference to the rendered QGraphicsPixmapItem + + :param int i: + :param int j: + :param int use_slice: + :return: tile or False + :rtype: QtWidgets.QGraphicsPixmapItem or False + """ + s = self.currentSlice if use_slice is None else use_slice + if s in self.tileWorkingSets \ + and i in self.tileWorkingSets[s] \ + and j in self.tileWorkingSets[s][i]: + return self.tileWorkingSets[s][i][j] + + return False + + def getTileDimensions(self, i, j, use_slice=None): + """ + returns dimensions of tile (i, j) in slice use_slice + @see self.getTile + :param int i: row + :param int j: line + :param int use_slice: slice + :return: (width, height) + :rtype: (int, int) or (bool, bool) + """ + + tile = self.getTile(i, j, use_slice) + if tile: + return tile['dimensions'] + return False, False + + def removeTile(self, i, j, use_slice=None): + """ + removes tile (i, j) of current (self.currentSlice) or given slice from scene + and removes references to Qt object from pyramid and list of currently rendered tiles + :param i: + :param j: + :param use_slice: + :return: + """ + tile = self.getTile(i, j, use_slice) + if tile['rendered']: + self.scene.removeItem(tile['rendered']) + self.currentTiles.remove(tile['rendered']) + tile['rendered'] = False + + def renderTile(self, i, j, x, y, force=False): + """ + renders tile (i, j) to scene at position (x, y), if tile (i, j), + saves a reference to a list of all currently rendered tiles and + to the tile entry in the (multidimensional) list of all tiles + if not rendered already (check omitted on force = True) + :param i: + :param j: + :param x: + :param y: + :param force: + :return: + """ + rendered = not not self.tileWorkingSets[self.currentSlice][i][j]['rendered'] + if not rendered or force: + if force: + self.removeTile(i, j) + + img = self.readViewTile(self.currentSlice, i, j) + item = QtWidgets.QGraphicsPixmapItem() + + # scale lower resolution tiles up (on slice > 0) + inv_scale_factor = (1 / self.scalingFactor) ** self.currentSlice + item.setScale(inv_scale_factor) + + # and adjust position of tile accordingly + x += i * (inv_scale_factor - 1) * int(self.tileDim[self.xIdx]) + y += j * (inv_scale_factor - 1) * int(self.tileDim[self.yIdx]) + + item.setPos(x, y) + + item.setAcceptedMouseButtons(QtCore.Qt.NoButton) + height, width, channel = img.shape + pix = QtGui.QPixmap() + pix.convertFromImage( + QtGui.QImage(img, width, height, 3 * width, QtGui.QImage.Format_RGB888) + ) + item.setPixmap(pix) + self.scene.addItem(item) + self.currentTiles[len(self.currentTiles):] = [item] + self.tileWorkingSets[self.currentSlice][i][j]['rendered'] = item + + def reset(self): + """ + @todo pydoc + :return: + """ + self.resetScene() + self.initDefaults() + + def resetScene(self): + """ + removes all tiles and its references from scene and list of currently rendered tiles + :return: + """ + for s in self.tileWorkingSets: + for i in self.tileWorkingSets[s]: + for j in self.tileWorkingSets[s][i]: + self.removeTile(i, j, use_slice=s) + # remove anything that may be left + [self.scene.removeItem(i) for i in self.currentTiles] + self.currentTiles = [] + + def fromDataset(self, dataset=None): + """ + set pyramid parameters loaded from dataset + :param dataset: + :return: + """ + if dataset is not None: + self.dataset = dataset + + if self.dataset is not None: + pyramid_params = self.dataset.getPyramidParams() + if pyramid_params is None: + self.reset() + else: + for i in self.datasetParams: + setattr(self, i, pyramid_params[i]) + self.tileWorkingSets = copy.deepcopy(self.tileSets) + + self.setMicroscopeMode(self.dataset.imagescanMode) + + def toDataset(self): + """ + saves pyramid params to dataset + :return: + """ + pyramid_params = {} + for i in self.datasetParams: + pyramid_params[i] = getattr(self, i) + + if self.dataset: + self.dataset.setPyramidParams(pyramid_params) + + def setMicroscopeMode(self, microscopemode): + """ + sets microscope mode + :param microscopemode: + :return: + """ + self.microscopeMode = microscopemode + + def getFullImage(self, scale): + """ + returns full size image + for this we use numpy to concatenate the tile images + :param: float scale + :return: full size image + :rtype: np.array + """ + first_col = True + img = None + col = None + # for big images use np.concatenate + # for each tile col + for x in range(len(self.tileWorkingSets[0])): + first_tile = True + # for each tile in col + for y in range(len(self.tileWorkingSets[0][x])): + tile = self.readViewTile(0, x, y) + if 1. != scale: + w = math.floor(scale * tile.shape[1]) + h = math.floor(scale * tile.shape[0]) + tile = np.array(Image.fromarray(tile).resize((w, h), resample=Image.BICUBIC)) + + if not first_tile: + col = np.concatenate((col, tile), axis=0) + else: + col = tile + first_tile = False + # self.imageOut(col, f"_col_{x}_{y}.tif") + + # self.imageOut(col, f"_col_{x}.tif") + if not first_col: + img = np.concatenate((img, col), axis=1) + else: + img = col + first_col = False + + # self.imageOut(img, f"_fullimage.tif") + return img + + def getBoundingRectDim(self): + """ + returns bounding rectangle of all currently rendered tiles + :return: (width, height) + :rtype: (int, int) + """ + group = self.scene.createItemGroup(self.currentTiles) + rect = group.boundingRect() + self.scene.destroyItemGroup(group) + return rect.width(), rect.height() + + def getSubImage(self, clip): + """ + :param (int, int, int, int) clip: (y1, y2, x1, x2) + :return: + :rtype: (np.array, QtGui.QPixmap) + """ + y1, y2, x1, x2 = clip + width = x2 - x1 + height = y2 - y1 + subimg = self.getImagePart(y1, y2, x1, x2) + + pix = QtGui.QPixmap() + pix.convertFromImage( + QtGui.QImage(subimg, width, height, 3 * width, QtGui.QImage.Format_RGB888) + ) + + return subimg, pix + + def addSrcTileSimple(self, img, pos, p): + """ + gets called by opticalscan.takePoint() in scan setup process + + adds a src tile by pos and dimension + """ + item = QtWidgets.QGraphicsPixmapItem() + item.setPos(pos[self.xIdx], pos[self.yIdx]) + item.setAcceptedMouseButtons(QtCore.Qt.NoButton) + height, width, channel = img.shape + pix = QtGui.QPixmap() + pix.convertFromImage( + QtGui.QImage(img, width, height, 3 * width, QtGui.QImage.Format_RGB888) + ) + item.setPixmap(pix) + + if 0 < len(self.currentTiles) and self.dataset.lastpos is not None: + lp = self.dataset.lastpos + dx, dy = (lp[0] - p[0]) / width * img.shape[1], (p[1] - lp[1]) / height * img.shape[0] + li: QtWidgets.QGraphicsItem = self.currentTiles[-1:][0] + newpos = li.pos() + newpos.setX(newpos.x() + dx) + newpos.setY(newpos.y() + dy) + li.setPos(newpos) + + self.scene.addItem(item) + self.currentTiles[len(self.currentTiles):] = [item] + + def addSrcTile(self, img, m_rot, v_trans, fullimgsize): + """ + gets called by opticalscan.loadAndPasteImage() while scanning takes place + adds a src tile by img data, transformation matrix, position, size of full image + :param img: + :param m_rot: + :param v_trans: + :param fullimgsize: + :return: + """ + self.preScanModeComplete = True + + self.fullImageWidth = current_full_image_width = fullimgsize[self.xIdx] + self.fullImageHeight = current_full_image_height = fullimgsize[self.yIdx] + + # width, height of view tiles + view_tile_width, view_tile_height = self.tileDim + + # dst pos of src tile + pos_x, pos_y = v_trans + + breakof_pixel_count = max(self.tileDim) * self.scalingFactor + current_scaling_factor = 1 + slice_nr = 0 + + # if src tile dim < view tile dim, a single src tile s may be part of up to 4 view tiles v(i, j) + while max(current_full_image_width, current_full_image_height) > breakof_pixel_count: + current_src_img = img.copy() + # resize from original image, when adding src tile to scaled pyramid slices + if slice_nr > 0: + current_src_img = cv2.resize( + img, + None, + fx=current_scaling_factor, + fy=current_scaling_factor, + interpolation=cv2.INTER_AREA + ) + + # width, height of src tile + src_tile_height, src_tile_width = current_src_img.shape[:2] + + # position of src tile in view tile + src_tile_pos_x = (current_scaling_factor * pos_x) % view_tile_width + src_tile_pos_y = (current_scaling_factor * pos_y) % view_tile_height + # does src tile exceed view tiles width? add exceeding parts to next (right) view tile + add_to_right = src_tile_pos_x + src_tile_width > view_tile_width + # does src tile exceed view tiles height? add exceeding parts to next (lower) view tile + add_to_below = src_tile_pos_y + src_tile_height > view_tile_height + # does src tile exceed view tiles height and width? add exceeding parts to next (right and lower) view tile + add_to_left_below = add_to_right and add_to_below + + # indices of the tile, the src tiles origin is located in + i = math.floor(current_scaling_factor * pos_x / view_tile_width) + j = math.floor(current_scaling_factor * pos_y / view_tile_height) + + # add to v(i , j) + tile = self.readViewTile(slice_nr, i, j) + size = (tile.shape[1], tile.shape[0]) + ''' + m = np.float32([ + [1, 0, src_tile_pos_x], + [0, 1, src_tile_pos_y] + ]) + ''' + + # adjust translation part of affine transformation matrix + m_rot[0][2] = src_tile_pos_x + m_rot[1][2] = src_tile_pos_y + m = m_rot + cv2.warpAffine(current_src_img, m, size, tile, borderMode=cv2.BORDER_TRANSPARENT) + self.saveViewTile(tile, slice_nr, i, j) + + # add to v(i+1, j) right of (i, j) + if add_to_right: + tile = self.readViewTile(slice_nr, i + 1, j) + size = (tile.shape[1], tile.shape[0]) + m = np.float32([ + [1, 0, src_tile_pos_x - view_tile_width], + [0, 1, src_tile_pos_y] + ]) + cv2.warpAffine(current_src_img, m, size, tile, borderMode=cv2.BORDER_TRANSPARENT) + self.saveViewTile(tile, slice_nr, i + 1, j) + + # add to v(i , j+1) below of (i, j) + if add_to_below: + tile = self.readViewTile(slice_nr, i, j + 1) + size = (tile.shape[1], tile.shape[0]) + m = np.float32([ + [1, 0, src_tile_pos_x], + [0, 1, src_tile_pos_y - view_tile_height] + ]) + cv2.warpAffine(current_src_img, m, size, tile, borderMode=cv2.BORDER_TRANSPARENT) + self.saveViewTile(tile, slice_nr, i, j + 1) + + # add to v(i+1, j+1) right and below of (i, j) + if add_to_left_below: + tile = self.readViewTile(slice_nr, i + 1, j + 1) + size = (tile.shape[1], tile.shape[0]) + m = np.float32([ + [1, 0, src_tile_pos_x - view_tile_width], + [0, 1, src_tile_pos_y - view_tile_height] + ]) + cv2.warpAffine(current_src_img, m, size, tile, borderMode=cv2.BORDER_TRANSPARENT) + self.saveViewTile(tile, slice_nr, i + 1, j + 1) + + current_scaling_factor *= self.scalingFactor + slice_nr += 1 + current_full_image_height *= self.scalingFactor + current_full_image_width *= self.scalingFactor + del current_src_img + + self.maxSliceNumber = slice_nr - 1 + self.updateSceneAfterViewTileUpdate(v_trans) + + def saveViewTile(self, img, slice_nr, i, j): + """ + saves tile to file system + :param img: + :param slice_nr: + :param i: + :param j: + :return: + """ + tile_path = os.path.join(self.dataset.getTilePath(), f"tile_{slice_nr}_{i}_{j}.tif") + cv2imwrite_fix(tile_path, cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) + + if slice_nr not in self.tileWorkingSets: + self.tileWorkingSets[slice_nr] = {} + self.tileSets[slice_nr] = {} + if i not in self.tileWorkingSets[slice_nr]: + self.tileWorkingSets[slice_nr][i] = {} + self.tileSets[slice_nr][i] = {} + if j not in self.tileWorkingSets[slice_nr][i]: + self.tileWorkingSets[slice_nr][i][j] = {"dimensions": (img.shape[1], img.shape[0]), "rendered": False} + self.tileSets[slice_nr][i][j] = {"dimensions": (img.shape[1], img.shape[0]), "rendered": False} + + def readViewTile(self, slice_nr, i, j): + """ + gets tile i, j of slice slice_nr + if file doesn't exist returns a correctly sized empty image + :param slice_nr: + :param i: + :param j: + :return: + """ + + tile_path = os.path.join(self.dataset.getTilePath(), f"tile_{slice_nr}_{i}_{j}.tif") + if not os.path.exists(tile_path): # + tile_path = os.path.join(self.dataset.getTilePath(), f"tile_{slice_nr}_{i}_{j}.bmp") + + try: + tile = cv2imread_fix(tile_path) + except OSError: + tile = None + + if tile is None: + # as full image size might not be divisible by tile dimensions, we have to determine correct tile size + # tile numbering is 0 based + scaling = self.scalingFactor ** slice_nr + max_x, max_y = self.tileDim + fiw = scaling * self.fullImageWidth + fih = scaling * self.fullImageHeight + + x = int(math.ceil(max(min(max_x, fiw - i * max_x), 0))) + y = int(math.ceil(max(min(max_y, fih - j * max_y), 0))) + tile = np.zeros((y, x, 3), np.uint8) + return tile + + def createFullImage(self): + """ + creates full size image of highest resolution + :return: + """ + width = self.fullImageWidth + height = self.fullImageHeight + fullsize_img = Image.new('RGB', (width, height), 'black') + + for i in self.tileWorkingSets[0]: + for j in self.tileWorkingSets[0][i]: + timg = Image.fromarray(self.readViewTile(0, i, j)) + pos = (i * self.tileDim[self.xIdx], j * self.tileDim[self.yIdx]) + fullsize_img.paste(timg, pos) + + fullsize_img.save(self.dataset.getImageName(), 'tiff', save_all=True, compression='tiff_deflate') + + @staticmethod + def createFromFullImage(dset): + """ + creates tiles and pyramid parameters from the full image + must load the full image which uses a large amount of ram + :param DataSet dset: + :return: + """ + src_img = dset.getImageName() + tile_base_path = dset.getTilePath() + + p = ScenePyramid.getDefaults() + tile_x = p['tileDim'][0] + tile_y = p['tileDim'][1] + scaling = p['scalingFactor'] + + warnings.simplefilter('ignore', Image.DecompressionBombError) + warnings.simplefilter('ignore', Image.DecompressionBombWarning) + # sqr(6GB / 3) ~ 45.000 x 45000 + Image.MAX_IMAGE_PIXELS = 6 * 1024 ** 3 + img = Image.open(src_img) + warnings.simplefilter('always', Image.DecompressionBombError) + warnings.simplefilter('always', Image.DecompressionBombWarning) + + p['preScanModeComplete'] = True + p['fullImageWidth'] = img.width + p['fullImageHeight'] = img.height + + ''' + 1. chop full image up in tiles + 2. scale full image down to new full image + 3. if max(width, height) of full image > breakof_pixel_count, go to 1. + ''' + slice_nr = 0 + breakof_pixel_count = max(tile_y, tile_x) + + while max(img.width, img.height) > breakof_pixel_count: + tiles_x = math.ceil(img.width / tile_x) + tiles_y = math.ceil(img.height / tile_y) + p['tileSets'][slice_nr] = {} + for (i, x) in zip(range(tiles_x), range(0, img.width, tile_x)): + p['tileSets'][slice_nr][i] = {} + for (j, y) in zip(range(tiles_y), range(0, img.height, tile_y)): + tile = img.crop(( + x, + y, + min(x + tile_x, img.width), + min(y + tile_y, img.height) + )) + tile_path = os.path.join(tile_base_path, f"tile_{slice_nr}_{i}_{j}.tif") + tile.save(tile_path, 'tiff', save_all=True, compression='tiff_deflate') + tile.close() + p['tileSets'][slice_nr][i][j] = { + "dimensions": (tile.width, tile.height), + "rendered": False + } + + img = img.resize(( + int(img.width * scaling), + int(img.height * scaling) + ), Image.LANCZOS) + slice_nr += 1 + + img.close() + p['maxSliceNumber'] = slice_nr - 1 + dset.setPyramidParams(p) + + def getImagePart(self, p0y, p1y, p0x, p1x): + """ + returns part of the full img + :param int ymin: + :param int ymax: + :param int xmin: + :param int xmax: + :return: + """ + ''' + we want to extract pixels of highest resolution from p0(xmin, ymin) to p1(xmax, ymax) + with + k(i,j) = tile at pos (i, j) + h, w = base height, base width of tile + we need to extract from subimage from tiles K where (i, j) in: + imin = floor(xmin/w) <= i <= floor(xmax/w) = imax + jmin = floor(ymin/h) <= j <= floor(ymax/h) = jmax + transform p0, p1 (relative to origin of tile k(imin, jmin)): + p0' = ( + xmin % w, + ymin % h + ) + p1' = ( + xmax % w + (imax - imin) * w, + ymax % h + (jmax - jmin) * h + ) + extract pixels + ''' + w = self.tileDim[self.xIdx] + h = self.tileDim[self.yIdx] + imin = math.floor(p0x / w) + imax = math.floor(p1x / w) + jmin = math.floor(p0y / h) + jmax = math.floor(p1y / h) + + ci = imax - imin + 1 + cj = jmax - jmin + 1 + + # p0 relative to origin of tile k(imin, jmin) + p0x_ = p0x % w + p0y_ = p0y % h + + m_trans = np.float32([[1, 0, 0], [0, 1, 0]]) + img = np.zeros((p1y - p0y, p1x - p0x, 3), np.uint8) + size = (p1x - p0x, p1y - p0y) + for i in range(ci): + for j in range(cj): + tile = self.readViewTile(0, imin + i, jmin + j) + # xmin of current tile in tile based sub image (relative to tile k(imin, jmin)) + xmin_t = i * w + # ymin + ymin_t = j * h + + # position in subimage to copy to + m_trans[0][2] = xmin_t - p0x_ + m_trans[1][2] = ymin_t - p0y_ + + cv2.warpAffine(tile, m_trans, size, img, flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_TRANSPARENT) + + # comment in(?) to check result in tile path + self.imageOut(img, f"_image_part.tif") + + return img + + def imageOut(self, img, name): + path = os.path.join(self.dataset.getTilePath(), name) + cv2imwrite_fix(path, cv2.cvtColor(img, cv2.COLOR_RGB2BGR)) + + def setTileOpacity(self, opacity): + """ + sets opacity of all currently shown tiles + :param float opacity: + :return: + """ + assert 0 <= opacity <= 1 + self.opacity = opacity + [tile.setOpacity(opacity) for tile in self.currentTiles] + + def destruct(self): + """ + removes all graphics items from scene + and saves itself to the dataset + :return: + """ + self.toDataset() + self.resetScene() -- GitLab