Location: Stomach Annotator for SPARC @ 9ea33dda840d / digitiser / qtdigitiser.py

Author:
rjag008 <rjag008@auckland.ac.nz>
Date:
2018-08-18 17:01:42+12:00
Desc:
Final Release
Permanent Source URI:
https://models.physiomeproject.org/workspace/51d/rawfile/9ea33dda840d259caf68e26d21300bece4eeff3f/digitiser/qtdigitiser.py

'''
Created on 15/06/2018

@author: rjag008
'''

    
from digitiser.marker import PaintGraphicsEllipseItem
from digitiser.dataset import PaintDatasetItem
from digitiser.view import PaintGraphicsView
from digitiser.model import PaintModel
from digitiser.scene import PaintGraphicsScene
import sys
from os import path
from collections import OrderedDict

dir_path = path.dirname(path.realpath(sys.argv[0]))
if not hasattr(sys, 'frozen'): #For py2exe
    dir_path = path.join(dir_path,"..")

uiFile = path.join(dir_path,"./digitiser/PaintUI.ui")

try:
    from PySide import QtCore, QtGui
    from pysideuiutils.uic import loadUi
    class PainterWidgetBase(QtGui.QWidget):
        def __init__(self, parent=None):
            QtGui.QWidget.__init__(self, parent)
            loadUi(uiFile, self)
            
except ImportError:
    #from PyQt4 import QtCore, QtGui, uic
    '''
    form,base = uic.loadUiType(uiFile)
    class PainterWidgetBase(base,form):
        def __init__(self,parent=None):
            super(base,self).__init__(parent)
            self.setupUi(self)
    '''
    pass




class PaintView(object):

    meshCoordinateScalingFactor = 250
    def __init__(self, model, graphicScene, listDatasets):
        self._model = model
        self._graphicScene = graphicScene
        self._listDatasets = listDatasets
        self.PenColors = [QtGui.QColor(col) for col in [ QtCore.Qt.red, QtCore.Qt.green, QtCore.Qt.blue,\
                                                         QtCore.Qt.cyan, QtCore.Qt.magenta, QtCore.Qt.yellow]]
        
        self.datasetColors = self._model.datasetColors
        if len(self.datasetColors)==0:
            self.datasetColors[0] = self.createPenAndMaterial(self.PenColors[0]) #Default items color
        self.reset()
        self._model.modelChanged.connect(self.modelChanged)
        self.modelChanged()
        self._listDatasets.itemChanged.connect(self.updateDatasetName)
        self._listDatasets.itemDoubleClicked.connect(self.changeItemColor)
        self._graphicsPaths = {}
        self.meshSilhouette  = None

    def changeItemColor(self,item):
        newColor = item.changeColor()
        if not newColor is None:
            ds = item._key
            #Change the color of related path and points
            self.datasetColors[ds] = self.createPenAndMaterial(newColor)
            for k in self._pointItems:
                item, dataset = self._pointItems[k]
                if dataset==ds:
                    item.setColor(newColor) 
            self.updatePath(ds)

    def setModel(self,model):
        self._model = model
        self.datasetColors = self._model.datasetColors

    def createPenAndMaterial(self,color):
        brush = QtGui.QBrush()
        bcolor = QtGui.QColor(color)
        bcolor.setAlpha(127)
        brush.setColor(bcolor)
        brush.setStyle(QtCore.Qt.CrossPattern)
        pen = QtGui.QPen(color, 1, QtCore.Qt.SolidLine,QtCore.Qt.FlatCap, QtCore.Qt.MiterJoin)
        return [pen,brush]

    def reset(self):
        self._graphicScene.clear()
        self._listDatasets.clear()

        background_file = self._model.getBackgroundFile()
        if background_file!='Default':
            pixmap = QtGui.QPixmap(background_file)
            self._background = self._graphicScene.addPixmap(pixmap)
        else:
            self._graphicScene.setBackgroundBrush(QtGui.QBrush(QtCore.Qt.white, QtCore.Qt.SolidPattern))
            self._background = None

        self._pointItems = {}
        self._datasetItems = {}
        self._graphicsPaths = {}
        self.meshSilhouette  = None
        
    def setMeshBackground(self,meshmapper,axialElements):
        '''
        Create a background based on the mesh  
        '''
        
        leftwall,rightwall,ladder = meshmapper.getBoundaryPoints()
        leftwall *=self.meshCoordinateScalingFactor
        rightwall *=self.meshCoordinateScalingFactor
        ladder *=self.meshCoordinateScalingFactor
        #Create lines to represent mesh
        #Create the brush
        color = QtGui.QColor(QtCore.Qt.black)
        pen = QtGui.QPen(color, 5, QtCore.Qt.SolidLine,QtCore.Qt.FlatCap, QtCore.Qt.MiterJoin)
        if hasattr(self, 'meshSilhouette'):
            if not self.meshSilhouette is None:
                self._graphicScene.removeItem(self.meshSilhouette)
        gpath = QtGui.QPainterPath()
        #First create points
        lpts = []
        rpts = []
        for i in range(axialElements+1):
            p1 = QtCore.QPointF(ladder[i,0], ladder[i,1])
            p2 = QtCore.QPointF(ladder[i,3], ladder[i,4])
            lpts.append(p1)
            rpts.append(p2)
        #Draw the ladder
        for i in range(axialElements+1):
            gpath.moveTo(lpts[i])
            gpath.quadTo(lpts[i],rpts[i])
        
        
        #Left wall
        lpts = []
        for coord in leftwall:
            p1 = QtCore.QPointF(coord[0],coord[1])
            lpts.append(p1)                    
        
        gpath.moveTo(lpts[0])
        for i in range(1,len(lpts)):
            gpath.quadTo(lpts[i-1], lpts[i])
        
        #Interpolate of right
        rrpts = []
        for coord in rightwall:
            p1 = QtCore.QPointF(coord[0],coord[1])
            rrpts.append(p1)
        gpath.moveTo(rrpts[0])
        for i in range(1,len(rrpts)):
            gpath.quadTo(rrpts[i-1], rrpts[i])
        
        
        gitem = self._graphicScene.addPath(gpath,pen)
        gitem.setEnabled(False)
        gitem.setZValue(0)
        self.meshSilhouette  = gitem
        self._model.setToScaffoldMeshScaling(self.meshCoordinateScalingFactor)
    
                
    def updateBackground(self):
        background_file = self._model.getBackgroundFile()
        if background_file != 'Default':
            pixmap = QtGui.QPixmap(background_file)
            zValue = 0
            if not self._background is None:
                zValue = self._background.zValue()
                self._graphicScene.removeItem(self._background)
            if hasattr(self, 'meshSilhouette'):
                if not self.meshSilhouette is None:
                    self._graphicScene.removeItem(self.meshSilhouette)
                self.meshSilhouette = None
                
            self._background = self._graphicScene.addPixmap(pixmap)
            self._background.setZValue(zValue)

    def modelChanged(self):
        self.updatePointItems()
        self.updateListDatasets()

    def updatePointItems(self):
        model_points = self._model.getPoints()
        set_model_keys = set(model_points.keys())
        set_view_keys = set(self._pointItems.keys())
        set_intersect_keys = set_model_keys.intersection(set_view_keys)
        # Added points
        added_keys = set_model_keys - set_intersect_keys
        for key in added_keys:
            pos, dataset = model_points[key]
            pos = QtCore.QPointF(pos[0], pos[1])
            self.addPointItem(key, pos, dataset)

        # Removed points
        removed_keys = set_view_keys - set_intersect_keys
        for key in removed_keys:
            self.removePointItem(key)

        # Potentially moved points
        for key in set_intersect_keys:
            model_pos = model_points[key][0]
            model_pos = QtCore.QPointF(model_pos[0], model_pos[1])
            item = self._pointItems[key][0]
            dist = (model_pos - item.scenePos()).manhattanLength()
            if dist > 0.001:
                item.setPos(model_pos)

    def addPointItem(self, key, pos, dataset):
        if dataset in self.datasetColors:
            pen = self.datasetColors[dataset][0]
        else:
            pen,brush = self.createPenAndMaterial(self.PenColors[dataset % len(self.PenColors)])
            self.datasetColors[dataset] = [pen,brush]
        item = PaintGraphicsEllipseItem(pen, key)
        item.setZValue(2)
        self._pointItems[key] = (item, dataset)
        self._graphicScene.addItem(item)
        item.setPos(pos)

    def removePointItem(self, key):
        item, _ = self._pointItems[key]
        self._graphicScene.removeItem(item)
        del self._pointItems[key]

    def listSelectionChanged(self,dataset):
        for k in self._pointItems:
            item,ds = self._pointItems[k]
            item.setEnabled(ds==dataset)

    def updateDatasetName(self,item):
        key = item._key
        if item._name != str(item.text()):
            item._name = str(item.text())
            self._model.changeDatasetName(key,str(item.text()))
        else:
            self.listItemVisibilityToggled(item)
            
    def updateListDatasets(self):
        model_datasets = self._model.getDatasets()
        set_model_keys = set(model_datasets.keys())
        set_view_keys = set(self._datasetItems.keys())
        set_intersect_keys = set_model_keys.intersection(set_view_keys)
        
        #Set the names to match for common keys
        def iterAllItems(self):
            for i in range(self.count()):
                yield self.item(i)

        for item in iterAllItems(self._listDatasets):
            if item._key in set_intersect_keys:
                name = model_datasets[item._key]
                item._name = name
                item.setText(name)
                    
        # Added datasets
        added_keys = set_model_keys - set_intersect_keys
        for key in added_keys:
            name = model_datasets[key]
            self.addListItem(key, name)

        # Removed datasets
        removed_keys = set_view_keys - set_intersect_keys
        for key in removed_keys:
            self.removeListItem(key)

        
        #Set current item
        key = self._model.getCurrentDataset()
        if not key is None:
            item = self._datasetItems[key]
            self._listDatasets.setCurrentItem(item)

    def addListItem(self, key, name):
        if key in self.datasetColors:
            pen = self.datasetColors[key][0]
        else:
            pen,brush = self.createPenAndMaterial(self.PenColors[key % len(self.PenColors)])
            self.datasetColors[key] = [pen,brush]
                    
        item = PaintDatasetItem(key, name, pen.color())
        self._datasetItems[key] = item
        self._listDatasets.addItem(item)

    def listItemVisibilityToggled(self,item):
        state = item.checkState()
        key = item._key
        if self.hasPath(key):
            self._graphicsPaths[key].setVisible(state)
        for k in self._pointItems:
            item,ds = self._pointItems[k]
            if ds==key:
                item.setVisible(state)
            
            
    def removeListItem(self, key):
        # TODO: Iterate  through items to find the one with the key
        def iterAllItems(self):
            for i in range(self.count()):
                yield self.item(i)

        for item in iterAllItems(self._listDatasets):
            if item._key == key:
                del item
                break
        if self.hasPath(key):
            self._graphicScene.removeItem(self._graphicsPaths[key])
        del self._datasetItems[key]

    def hasPath(self,dataset):
        return dataset in self._graphicsPaths

    def updatePath(self,dataset):
        try:
            if dataset in self._graphicsPaths:
                try:
                    self._graphicScene.removeItem(self._graphicsPaths[dataset])
                except:
                    pass
                del self._graphicsPaths[dataset]
            mpts = self._model.getPointsForDataset(dataset)
            modelPoints = [QtCore.QPointF(pos[0], pos[1]) for pos in mpts]
            if len(modelPoints) >=2:
                #zval = min([item.zValue() for item in items])
                gpath = QtGui.QPainterPath()
                gpath.moveTo(modelPoints[0])
                for i in range(1,len(modelPoints)):
                    gpath.quadTo(modelPoints[i-1], modelPoints[i])
                gpath.closeSubpath()
                #gpath.quadTo(modelPoints[-1], modelPoints[0])
                #pen,brush = self.fillBrushes[dataset % len(self.PenColors)]
                pen,brush = self.datasetColors[dataset]
                gitem = self._graphicScene.addPath(gpath,pen,brush)
                gitem.setZValue(1)
                self._graphicsPaths[dataset] = gitem
        except:
            import traceback
            traceback.print_exc(file=sys.stdout)



class PainterWidget(PainterWidgetBase):
    region, point = range(2)
    flipped = False
    def __init__(self,mode,parent=None,project=None):
        super(PainterWidget,self).__init__(parent)
        self.sceneLayout = QtGui.QVBoxLayout(self.graphicsViewHolder)
        self.graphicsView = PaintGraphicsView()
        self.sceneLayout.addWidget(self.graphicsView)
        
        self.filename = None
        self.model = PaintModel()
        self.model.modelChanged.connect(self.updateInterface)
        
        self._graphicsScene = PaintGraphicsScene(self)
        self.graphicsView.setScene(self._graphicsScene)

        self.addNewDataset.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_FileDialogNewFolder')))
        self.removeCurrentDataset.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_MessageBoxCritical')))
        self.moveUp.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_ArrowUp')))
        self.moveDown.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_ArrowDown')))
        self.saveData.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_DialogSaveButton')))
        self.loadData.setIcon(self.style().standardIcon(getattr(QtGui.QStyle, 'SP_DialogOpenButton')))
        
        self.addNewDataset.clicked.connect(self.addDataset)
        self.removeCurrentDataset.clicked.connect(self.removeDataset)
        #self.loadBackground.clicked.connect(self.newBackground)
        self.saveData.clicked.connect(self.saveRegions)
        self.loadData.clicked.connect(self.loadRegions)
        
        self.moveUp.clicked.connect(self.moveDatasetItemUp)
        self.moveDown.clicked.connect(self.moveDatasetItemDown)
        # Actions
        self.listDatasets.itemSelectionChanged.connect(self.selectedDatasetChanged)
        self._mode = mode
        if mode==self.region:
            self.viewMousePressEvent = self.viewMousePressEventRegion
            self.pointItemMoved = self.pointItemMovedRegion
        else:
            self.viewMousePressEvent = self.viewMousePressEventPoint
            self.pointItemMoved = self.pointItemMovedPoint
        # Mouse move
        self._graphicsScene.mousePressed.connect(self.viewMousePressEvent)
        self._graphicsScene.pointItemMoved.connect(self.pointItemMoved)
        self.loadproject(project)


    def moveDatasetItemUp(self):
        numItems = self.listDatasets.count()
        if numItems>1:
            currentRow = self.listDatasets.currentRow()
            if currentRow > 0:
                ci = self.listDatasets.takeItem(currentRow)
                self.listDatasets.insertItem(currentRow - 1, ci)
                self.listDatasets.setCurrentRow(currentRow - 1)

    def moveDatasetItemDown(self):
        numItems = self.listDatasets.count()
        if numItems>1:
            currentRow = self.listDatasets.currentRow()
            if currentRow < numItems-1:
                ci = self.listDatasets.takeItem(currentRow)
                self.listDatasets.insertItem(currentRow + 1, ci)
                self.listDatasets.setCurrentRow(currentRow + 1)

    def setupMeshBackground(self,backgroundMesh,axialElements):
        '''
        Load a mesh and create a background based on the node that lie on xy-plane
        Assumes the standard Stomach Meshobject is used 
        '''
        self.model.newDataSet('Default')
        self.view = PaintView(self.model, self._graphicsScene, self.listDatasets)
        self.view.setMeshBackground(backgroundMesh,axialElements)
        #Rendering needs to be flipped along y, as qgraphicsscene coordinate system starts at top,left
        if not self.flipped:
            self.graphicsView.setflip(-1)
            self.flipped = True
        self.filename = None
        self.updateInterface()
    
    def getMarkersAndColors(self):
        result = self.model.getDatasetInTransformedCoordinates()
        #Order the result in the listed order
        orderedResult = OrderedDict()
        for row in range(self.listDatasets.count()):
            item = self.listDatasets.item(row)
            name = item._name
            orderedResult[name] = result[name]
        orderedColors = OrderedDict()
        for k,name in self.model._datasets.items():
            p,_ = self.model.datasetColors[k]
            color = p.color()
            orderedColors[name] = [color.red(),color.green(),color.blue()]

        return orderedResult,orderedColors        
    
    def saveRegions(self,filename=None):
        if hasattr(self, 'view'):
            if filename is None:
                filename = QtGui.QFileDialog.getSaveFileName(self,
                    'Dataset file', '.',
                    "Data Files (*.dat)")
                if isinstance(filename,tuple): #Handle pyside
                    filename = str(filename[0])        
            if filename is not None and not str(filename) == "":
                return self.model.saveDataset(filename)
        return False
            
    def loadRegions(self,filename=None):
        if hasattr(self, 'view'):
            if filename is None:
                filename = QtGui.QFileDialog.getOpenFileName(self,
                    'Dataset file', '.',
                    "Data Files (*.dat)")
                if isinstance(filename,tuple): #Handle pyside
                    filename = str(filename[0])
            if filename is not None and not str(filename) == "":
                self.model.loadDataset(filename)
                self.view.setModel(self.model)
                if self._mode==self.region:
                    for dataset in self.model._datasets:
                        self.view.updatePath(dataset)
        

    def newBackground(self):
        filename = QtGui.QFileDialog.getOpenFileName(self,
            'Select background image', '.',
            "Image Files (*.png *.jpg *.jpeg *.bmp *.tif)")
        if isinstance(filename,tuple): #Handle pyside
            filename = str(filename[0])
        
        if filename is not None and not str(filename) == "":
            # Clean previous state
            if hasattr(self, 'view'):
                if hasattr(self.view, 'meshSilhouette') and not self.view.meshSilhouette is None :
                    if self.flipped:
                        self.graphicsView.setflip(-1)
                        self.flipped = False                
                self.model.addImage(str(filename))
                self.view.updateBackground()
            else:
                self.model.newDataSet(filename)
                self.view = PaintView(self.model, self._graphicsScene, self.listDatasets)
            self.filename = filename
        else:
            self.filename = None
                
        self.updateInterface()
    
    def defaultProject(self):
        self.model.newDataSet('Default')
        self.view = PaintView(self.model, self._graphicsScene, self.listDatasets)
        self.filename = None
        self.updateInterface()
        

    def loadproject(self,filename):
        if filename is not None and not str(filename) == "":
            # Try to load project
            self.model.loadDataset(filename)
            self.view = PaintView(self.model, self._graphicsScene, self.listDatasets)
            self.filename = filename
            self.updateInterface()


    def updateInterface(self):
        self.graphicsView.setEnabled(True)

    def addDataset(self):
        self.model.addDataset()

    def removeDataset(self):
        row = self.listDatasets.currentRow()
        item = self.listDatasets.takeItem(row)
        self.model.removeDataset(item._key)

    def selectedDatasetChanged(self):
        if not self.listDatasets.currentItem() is None:
            ds = self.listDatasets.currentItem()._key
            self.model.changeCurrentDataset(ds)
            if hasattr(self, 'view'):
                self.view.listSelectionChanged(ds)
        else:
            self.model.changeCurrentDataset(None)

    def viewMousePressEventRegion(self, event, item):
        mouse_pos = event.scenePos();
        mouse_pos = [mouse_pos.x(), mouse_pos.y()]
        #mode = self.model._mode
        currentDataset = self.model.getCurrentDataset()
        if event.button() == QtCore.Qt.LeftButton:
            if (item is None) or (item == self.view._background) or (item == self.view.meshSilhouette):
                    self.model.addPoint(mouse_pos)
            elif isinstance(item, PaintGraphicsEllipseItem) and not self.view.hasPath(currentDataset):
                self.view.updatePath(currentDataset)
        elif event.button() == QtCore.Qt.RightButton:
            if (not item is None) and (item != self.view._background) and (item != self.view.meshSilhouette):
                if isinstance(item, PaintGraphicsEllipseItem):
                    key = item.getItemKey()
                    self.model.removePoint(key)
                    if self.view.hasPath(currentDataset):
                        self.view.updatePath(self.model.getCurrentDataset())

    def viewMousePressEventPoint(self, event, item):
        mouse_pos = event.scenePos();
        mouse_pos = [mouse_pos.x(), mouse_pos.y()]
        if event.button() == QtCore.Qt.LeftButton:
            if (item is None) or (item == self.view._background):
                    self.model.addPoint(mouse_pos)
        elif event.button() == QtCore.Qt.RightButton:
            if (not item is None) and (item != self.view._background):
                if isinstance(item, PaintGraphicsEllipseItem):
                    key = item.getItemKey()
                    self.model.removePoint(key)


    def pointItemMovedRegion(self, key, pos):
        self.model.movePoint(key, [pos.x(), pos.y()])
        self.view.updatePath(self.model.getCurrentDataset())
        
    def pointItemMovedPoint(self, key, pos):
        self.model.movePoint(key, [pos.x(), pos.y()])

from digitiser.mapping import MeshMapper
if __name__ == '__main__':
    # Entry point
    application = QtGui.QApplication(sys.argv)
    main_window = PainterWidget(0)
    main_window.show()
    #main_window.defaultProject()
    backgroundMesh = MeshMapper()
    #self.backgroundMesh.setupByFile(r'scaffoldmaker.ex2', 8,11)
    backgroundMesh.setupByPickle('symmetricstomachsurface.pkl')
    
    main_window.setupMeshBackground(backgroundMesh,11)
    #main_window.newBackground()
    
    sys.exit(application.exec_())