Exemplo n.º 1
0
class CIP_ParenchymaSubtypeTrainingWidget(ScriptedLoadableModuleWidget):
    """Uses ScriptedLoadableModuleWidget base class, available at:
    https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
    """
    @property
    def moduleName(self):
        return "CIP_ParenchymaSubtypeTraining"

    def __init__(self, parent):
        ScriptedLoadableModuleWidget.__init__(self, parent)

        from functools import partial

        def __onNodeAddedObserver__(self, caller, eventId, callData):
            """Node added to the Slicer scene"""
            if callData.GetClassName() == 'vtkMRMLScalarVolumeNode' \
                    and slicer.util.mainWindow().moduleSelector().selectedModule == self.moduleName:
                self.__onNewVolumeLoaded__(callData)
            # elif callData.GetClassName() == 'vtkMRMLLabelMapVolumeNode':
            #     self.__onNewLabelmapLoaded__(callData)

        self.__onNodeAddedObserver__ = partial(__onNodeAddedObserver__, self)
        self.__onNodeAddedObserver__.CallDataType = vtk.VTK_OBJECT

    def setup(self):
        """This is called one time when the module GUI is initialized
        """
        ScriptedLoadableModuleWidget.setup(self)

        # Create objects that can be used anywhere in the module. Example: in most cases there should be just one
        # object of the logic class
        self.logic = CIP_ParenchymaSubtypeTrainingLogic()
        self.currentVolumeLoaded = None
        self.blockNodeEvents = False

        ##########
        # Main area
        self.mainAreaCollapsibleButton = ctk.ctkCollapsibleButton()
        self.mainAreaCollapsibleButton.text = "Main area"
        self.layout.addWidget(self.mainAreaCollapsibleButton,
                              SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.mainLayout = qt.QGridLayout(self.mainAreaCollapsibleButton)

        # Node selector
        volumeLabel = qt.QLabel("Active volume: ")
        volumeLabel.setStyleSheet("margin-left:5px")
        self.mainLayout.addWidget(volumeLabel, 0, 0)
        self.volumeSelector = slicer.qMRMLNodeComboBox()
        self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "")
        self.volumeSelector.selectNodeUponCreation = True
        self.volumeSelector.autoFillBackground = True
        self.volumeSelector.addEnabled = False
        self.volumeSelector.noneEnabled = False
        self.volumeSelector.removeEnabled = False
        self.volumeSelector.showHidden = False
        self.volumeSelector.showChildNodeTypes = False
        self.volumeSelector.setMRMLScene(slicer.mrmlScene)
        self.volumeSelector.setFixedWidth(250)
        self.volumeSelector.setStyleSheet("margin: 15px 0")
        #self.volumeSelector.selectNodeUponCreation = False
        self.mainLayout.addWidget(self.volumeSelector, 0, 1, 1, 3)

        labelsStyle = "font-weight: bold; margin: 0 0 5px 5px;"
        # Types Radio Buttons
        typesLabel = qt.QLabel("Select the type")
        typesLabel.setStyleSheet(labelsStyle)
        typesLabel.setFixedHeight(15)
        self.mainLayout.addWidget(typesLabel, 1, 0)
        self.typesFrame = qt.QFrame()
        self.typesLayout = qt.QVBoxLayout(self.typesFrame)
        self.mainLayout.addWidget(self.typesFrame, 2, 0,
                                  SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.typesRadioButtonGroup = qt.QButtonGroup()
        for key in self.logic.params.mainTypes.iterkeys():
            rbitem = qt.QRadioButton(self.logic.params.getMainTypeLabel(key))
            self.typesRadioButtonGroup.addButton(rbitem, key)
            self.typesLayout.addWidget(rbitem)
        self.typesRadioButtonGroup.buttons()[0].setChecked(True)

        # Subtypes Radio buttons
        subtypesLabel = qt.QLabel("Select the subtype")
        subtypesLabel.setStyleSheet(labelsStyle)
        subtypesLabel.setFixedHeight(15)
        self.mainLayout.addWidget(subtypesLabel, 1, 1)
        self.subtypesRadioButtonGroup = qt.QButtonGroup()
        self.subtypesFrame = qt.QFrame()
        self.subtypesFrame.setMinimumHeight(275)
        self.subtypesLayout = qt.QVBoxLayout(self.subtypesFrame)
        self.subtypesLayout.setAlignment(SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.subtypesLayout.setStretch(0, 0)
        self.mainLayout.addWidget(
            self.subtypesFrame, 2, 1,
            SlicerUtil.ALIGNMENT_VERTICAL_TOP)  # Put the frame in the top
        # The content will be loaded dynamically every time the main type is modified

        # Artifact radio buttons
        self.artifactsLabel = qt.QLabel("Artifact")
        self.artifactsLabel.setStyleSheet(labelsStyle)
        self.artifactsLabel.setFixedHeight(15)
        self.mainLayout.addWidget(self.artifactsLabel, 1, 2)
        #self.mainLayout.addWidget(qt.QLabel("Select the artifact"), 1, 1)
        self.artifactsRadioButtonGroup = qt.QButtonGroup()
        self.artifactsFrame = qt.QFrame()
        self.artifactsLayout = qt.QVBoxLayout(self.artifactsFrame)
        self.mainLayout.addWidget(self.artifactsFrame, 2, 2,
                                  SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.artifactsRadioButtonGroup = qt.QButtonGroup()
        for artifactId in self.logic.params.artifacts.iterkeys():
            rbitem = qt.QRadioButton(
                self.logic.params.getArtifactLabel(artifactId))
            self.artifactsRadioButtonGroup.addButton(rbitem, artifactId)
            self.artifactsLayout.addWidget(rbitem)
        self.artifactsRadioButtonGroup.buttons()[0].setChecked(True)

        # Load caselist button
        self.loadButton = ctk.ctkPushButton()
        self.loadButton.text = "Load fiducials file"
        self.loadButton.setIcon(
            qt.QIcon("{0}/open_file.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.loadButton.setIconSize(qt.QSize(20, 20))
        self.loadButton.setFixedWidth(135)
        self.mainLayout.addWidget(self.loadButton, 3, 0)

        # Remove fiducial button
        self.removeLastFiducialButton = ctk.ctkPushButton()
        self.removeLastFiducialButton.text = "Remove last fiducial"
        self.removeLastFiducialButton.toolTip = "Remove the last fiducial added"
        self.removeLastFiducialButton.setIcon(
            qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.removeLastFiducialButton.setIconSize(qt.QSize(20, 20))
        self.removeLastFiducialButton.setFixedWidth(200)
        self.mainLayout.addWidget(self.removeLastFiducialButton, 3, 1)

        # Save results button
        self.saveResultsButton = ctk.ctkPushButton()
        self.saveResultsButton.setText("Save markups")
        self.saveResultsButton.toolTip = "Save the markups in the specified directory"
        self.saveResultsButton.setIcon(
            qt.QIcon("{0}/Save.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.saveResultsButton.setIconSize(qt.QSize(20, 20))
        self.mainLayout.addWidget(self.saveResultsButton, 4, 0)

        # Save results directory button
        defaultPath = os.path.join(
            SlicerUtil.getSettingsDataFolder(self.moduleName),
            "results")  # Assign a default path for the results
        path = SlicerUtil.settingGetOrSetDefault(self.moduleName,
                                                 "SaveResultsDirectory",
                                                 defaultPath)
        self.saveResultsDirectoryButton = ctk.ctkDirectoryButton()
        self.saveResultsDirectoryButton.directory = path
        self.saveResultsDirectoryButton.setMaximumWidth(375)
        self.mainLayout.addWidget(self.saveResultsDirectoryButton, 4, 1, 1, 2)

        #####
        # Case navigator
        self.caseNavigatorWidget = None
        if SlicerUtil.isSlicerACILLoaded():
            caseNavigatorAreaCollapsibleButton = ctk.ctkCollapsibleButton()
            caseNavigatorAreaCollapsibleButton.text = "Case navigator"
            self.layout.addWidget(caseNavigatorAreaCollapsibleButton, 0x0020)
            # caseNavigatorLayout = qt.QVBoxLayout(caseNavigatorAreaCollapsibleButton)

            # Add a case list navigator
            from ACIL.ui import CaseNavigatorWidget
            self.caseNavigatorWidget = CaseNavigatorWidget(
                self.moduleName, caseNavigatorAreaCollapsibleButton)
            self.caseNavigatorWidget.setup()
            # Listen for the event of loading a new labelmap
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_LABELMAP_LOADED,
                self.__onNewILDClassificationLabelmapLoaded__)

        self.layout.addStretch()

        self.updateState()

        # Connections
        self.typesRadioButtonGroup.connect("buttonClicked (QAbstractButton*)",
                                           self.__onTypesRadioButtonClicked__)
        self.subtypesRadioButtonGroup.connect(
            "buttonClicked (QAbstractButton*)",
            self.__onSubtypesRadioButtonClicked__)
        self.artifactsRadioButtonGroup.connect(
            "buttonClicked (QAbstractButton*)",
            self.__onSubtypesRadioButtonClicked__)

        self.volumeSelector.connect('currentNodeChanged(vtkMRMLNode*)',
                                    self.__onCurrentNodeChanged__)
        self.loadButton.connect('clicked()', self.openFiducialsFile)
        self.removeLastFiducialButton.connect(
            'clicked()', self.__onRemoveLastFiducialButtonClicked__)
        # self.saveResultsOpenDirectoryDialogButton.connect('clicked()', self.onOpenDirectoryDialogButtonClicked)
        self.saveResultsDirectoryButton.connect(
            "directoryChanged (QString)",
            self.__onSaveResultsDirectoryChanged__)
        self.saveResultsButton.connect('clicked()',
                                       self.__onSaveResultsButtonClicked__)

        self.observers = []
        self.observers.append(
            slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent,
                                         self.__onNodeAddedObserver__))
        self.observers.append(
            slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent,
                                         self.__onSceneClosed__))

    def updateState(self):
        """ Refresh the markups state, activate the right fiducials list node (depending on the
        current selected type) and creates it when necessary
        :return:
        """
        # Load the subtypes for this type
        subtypesDict = self.logic.getSubtypes(
            self.typesRadioButtonGroup.checkedId())
        # Remove all the existing buttons
        for b in self.subtypesRadioButtonGroup.buttons():
            b.hide()
            b.delete()
        # Add all the subtypes with the full description
        for subtype in subtypesDict.iterkeys():
            rbitem = qt.QRadioButton(
                self.logic.getSubtypeFullDescription(subtype))
            self.subtypesRadioButtonGroup.addButton(rbitem, subtype)
            self.subtypesLayout.addWidget(rbitem,
                                          SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        # Check first element by default
        self.subtypesRadioButtonGroup.buttons()[0].setChecked(True)

        # Set the correct state for fiducials
        if self.currentVolumeLoaded is not None:
            self.logic.setActiveFiducialsListNode(
                self.currentVolumeLoaded,
                self.typesRadioButtonGroup.checkedId(),
                self.subtypesRadioButtonGroup.checkedId(),
                self.artifactsRadioButtonGroup.checkedId())

    def saveResultsCurrentNode(self):
        """ Get current active node and save the xml fiducials file
        """
        try:
            d = self.saveResultsDirectoryButton.directory
            if not os.path.isdir(d):
                # Ask the user if he wants to create the folder
                if qt.QMessageBox.question(
                        slicer.util.mainWindow(), "Create directory?",
                        "The directory '{0}' does not exist. Do you want to create it?"
                        .format(d), qt.QMessageBox.Yes
                        | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                    try:
                        os.makedirs(d)
                        # Make sure that everybody has write permissions (sometimes there are problems because of umask)
                        os.chmod(d, 0777)
                    except:
                        qt.QMessageBox.warning(
                            slicer.util.mainWindow(), 'Directory incorrect',
                            'The folder "{0}" could not be created. Please select a valid directory'
                            .format(d))
                        return
                    self.logic.saveCurrentFiducials(d,
                                                    self.caseNavigatorWidget,
                                                    self.uploadFileResult)
                    qt.QMessageBox.information(
                        slicer.util.mainWindow(), 'Results saved',
                        "The results have been saved succesfully")
            else:
                self.logic.saveCurrentFiducials(d, self.caseNavigatorWidget,
                                                self.uploadFileResult)
                qt.QMessageBox.information(
                    slicer.util.mainWindow(), 'Results saved',
                    "The results have been saved succesfully")
        except:
            Util.print_last_exception()
            qt.QMessageBox.critical(
                slicer.util.mainWindow(), "Error when saving the results",
                "Error when saving the results. Please review the console for additional info"
            )

    def uploadFileResult(self, result):
        if result != Util.OK:
            qt.QMessageBox.warning(
                slicer.util.mainWindow(), "Error when uploading fiducials",
                "There was an error when uploading the fiducials file. This doesn't mean that your file wasn't saved locally!\n"
                + "Please review the console for more information")

    def openFiducialsFile(self):
        volumeNode = self.volumeSelector.currentNode()
        if volumeNode is None:
            qt.QMessageBox.warning(slicer.util.mainWindow(), 'Select a volume',
                                   'Please load a volume first')
            return

        f = qt.QFileDialog.getOpenFileName()
        if f:
            self.logic.loadFiducialsXml(volumeNode, f)
            self.saveResultsDirectoryButton.directory = os.path.dirname(f)
            qt.QMessageBox.information(slicer.util.mainWindow(), "File loaded",
                                       "File loaded successfully")

    ## PRIVATE METHODS
    def __checkNewVolume__(self, newVolumeNode):
        """ New volume loaded in the scene in some way.
        If it's really a new volume, try to save and close the current one
        @param newVolumeNode:
        """
        if self.blockNodeEvents:
            return
        self.blockNodeEvents = True
        volume = self.currentVolumeLoaded
        if volume is not None and newVolumeNode is not None \
                and newVolumeNode.GetID() != volume.GetID()  \
                and not self.logic.isVolumeSaved(volume.GetName()):
            # Ask the user if he wants to save the previously loaded volume
            if qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save results?",
                    "The fiducials for the volume '{0}' have not been saved. Do you want to save them?"
                    .format(volume.GetName()), qt.QMessageBox.Yes
                    | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                self.saveResultsCurrentNode()
        # Remove all the previously existing nodes
        if self.currentVolumeLoaded is not None and newVolumeNode != self.currentVolumeLoaded:
            # Remove previously existing node
            self.logic.removeMarkupsAndNode(self.currentVolumeLoaded)
        if self.caseNavigatorWidget is not None and newVolumeNode is not None:
            # Try to load a previously existing fiducials file downloaded with the ACIL case navigator
            fiducialsFileName = newVolumeNode.GetName(
            ) + Util.file_conventions_extensions[
                "ParenchymaTrainingFiducialsXml"]
            fiducialsNavigatorFilePath = self.caseNavigatorWidget.logic.getFilePath(
                fiducialsFileName)
            if os.path.exists(fiducialsNavigatorFilePath):
                # The fiducials file was downloaded with the navigator
                self.logic.loadFiducialsXml(newVolumeNode,
                                            fiducialsNavigatorFilePath)

        if newVolumeNode is not None:
            SlicerUtil.setActiveVolumeId(newVolumeNode.GetID())
            SlicerUtil.setFiducialsCursorMode(True, True)

        self.currentVolumeLoaded = newVolumeNode
        self.updateState()
        self.blockNodeEvents = False

    def __getColorTable__(self):
        """ Color table for this module for a better labelmap visualization.
        This method is optimized for ILD classification, but it can easily be extended for different visualizations"""
        colorTableNode = slicer.util.getNode("CIP_ILDClassification_ColorMap*")
        if colorTableNode is None:
            # Load the node from disk
            p = os.path.join(os.path.dirname(os.path.realpath(__file__)),
                             "Resources/CIP_ILDClassification_ColorMap.ctbl")
            colorTableNode = slicer.modules.colors.logic().LoadColorFile(p)
        return colorTableNode

    ## EVENTS
    def enter(self):
        """This is invoked every time that we select this module as the active module in Slicer (not only the first time)"""
        self.blockNodeEvents = False
        # if len(self.observers) == 0:
        #     self.observers.append(slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, self.__onNodeAddedObserver__))
        #     self.observers.append(slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent, self.__onSceneClosed__))
        SlicerUtil.setFiducialsCursorMode(True, True)

        if self.volumeSelector.currentNodeId != "":
            SlicerUtil.setActiveVolumeId(self.volumeSelector.currentNodeId)
            self.currentVolumeLoaded = slicer.mrmlScene.GetNodeByID(
                self.volumeSelector.currentNodeId)
            self.updateState()

    def exit(self):
        """This is invoked every time that we switch to another module (not only when Slicer is closed)."""
        try:
            self.blockNodeEvents = True
            SlicerUtil.setFiducialsCursorMode(False)
            # for observer in self.observers:
            #     slicer.mrmlScene.RemoveObserver(observer)
            #     self.observers.remove(observer)
        except:
            pass

    def cleanup(self):
        """This is invoked as a destructor of the GUI when the module is no longer going to be used"""
        try:
            for observer in self.observers:
                slicer.mrmlScene.RemoveObserver(observer)
                self.observers.remove(observer)
        except:
            pass

    def __onNewVolumeLoaded__(self, newVolumeNode):
        """ Added a new node in the scene
        :param newVolumeNode:
        :return:
        """
        # Filter the name of the volume to remove possible suffixes added by Slicer
        filteredName = SlicerUtil.filterVolumeName(newVolumeNode.GetName())
        newVolumeNode.SetName(filteredName)
        self.__checkNewVolume__(newVolumeNode)
        self.blockNodeEvents = True
        self.volumeSelector.setCurrentNode(newVolumeNode)
        self.blockNodeEvents = False

    def __onNewILDClassificationLabelmapLoaded__(self, labelmapNode, split1,
                                                 split2):
        """ Load a new ILD classification labelmap volume.
        If the labelmap is a known labelmap type, set the right colors and opacity
        @param labelmapNode:
        """
        if SlicerUtil.isExtensionMatch(labelmapNode,
                                       "ILDClassificationLabelmap"):
            colorNode = self.__getColorTable__()
            displayNode = labelmapNode.GetDisplayNode()
            displayNode.SetAndObserveColorNodeID(colorNode.GetID())
            # Change Opacity
            SlicerUtil.displayLabelmapVolume(labelmapNode.GetID(), 0.3)

    def __onCurrentNodeChanged__(self, volumeNode):
        self.__checkNewVolume__(volumeNode)
        # if volumeNode:
        #     #self.logic.reset(volumeNode)
        #     SlicerUtil.setActiveVolumeId(volumeNode.GetID())
        #     self.updateState()

    def __onTypesRadioButtonClicked__(self, button):
        """ One of the radio buttons has been pressed
        :param button:
        :return:
        """
        self.updateState()

    def __onSubtypesRadioButtonClicked__(self, button):
        """ One of the subtype radio buttons has been pressed
        :param button:
        :return:
        """
        selectedVolume = self.volumeSelector.currentNode()
        if selectedVolume is not None:
            self.logic.setActiveFiducialsListNode(
                selectedVolume, self.typesRadioButtonGroup.checkedId(),
                self.subtypesRadioButtonGroup.checkedId(),
                self.artifactsRadioButtonGroup.checkedId())

    def __onRemoveLastFiducialButtonClicked__(self):
        self.logic.removeLastMarkup()

    def __onSaveResultsDirectoryChanged__(self, directory):
        # f = qt.QFileDialog.getExistingDirectory()
        # if f:
        #     self.saveResultsDirectoryText.setText(f)
        SlicerUtil.setSetting(self.moduleName, "SaveResultsDirectory",
                              directory)

    def __onSaveResultsButtonClicked__(self):
        self.saveResultsCurrentNode()

    def __onSceneClosed__(self, arg1, arg2):
        self.currentVolumeLoaded = None
        self.logic = CIP_ParenchymaSubtypeTrainingLogic()
Exemplo n.º 2
0
class CIP_ParenchymaSubtypeTrainingLabellingWidget(ScriptedLoadableModuleWidget
                                                   ):
    """GUI object"""
    def __init__(self, parent):
        ScriptedLoadableModuleWidget.__init__(self, parent)

        # from functools import partial
        # def __onNodeAddedObserver__(self, caller, eventId, callData):
        #     """Node added to the Slicer scene"""
        #     # if callData.GetClassName() == 'vtkMRMLScalarVolumeNode' \
        #     #         and slicer.util.mainWindow().moduleSelector().selectedModule == self.moduleName:
        #     #     self.__onNewVolumeLoaded__(callData)
        #     #if callData.GetClassName() == 'vtkMRMLLabelMapVolumeNode':
        #     self._onNewLabelmapLoaded_(callData)
        #
        #
        # self.__onNodeAddedObserver__ = partial(__onNodeAddedObserver__, self)
        # self.__onNodeAddedObserver__.CallDataType = vtk.VTK_OBJECT

        self.firstLoad = True
        self.activeEditorTools = None
        self.pendingChangesIdsList = []

    @property
    def labelmapNodeNameExtension(self):
        return "parenchymaTrainingLabelMap"

    ################
    # Main methods
    ################
    def setup(self):
        """Init the widget """
        # self.firstLoad = True
        ScriptedLoadableModuleWidget.setup(self)

        # Create objects that can be used anywhere in the module. Example: in most cases there should be just one
        # object of the logic class
        self._initLogic_()

        ##########
        # Main area
        self.mainAreaCollapsibleButton = ctk.ctkCollapsibleButton()
        self.mainAreaCollapsibleButton.text = "Main area"
        self.layout.addWidget(self.mainAreaCollapsibleButton,
                              SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.mainLayout = qt.QGridLayout(self.mainAreaCollapsibleButton)

        # Node selector
        volumeLabel = qt.QLabel("Active volume: ")
        volumeLabel.setStyleSheet("margin-left:5px")
        self.mainLayout.addWidget(volumeLabel, 0, 0)
        self.volumeSelector = slicer.qMRMLNodeComboBox()
        self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "")
        self.volumeSelector.selectNodeUponCreation = True
        self.volumeSelector.autoFillBackground = True
        self.volumeSelector.addEnabled = False
        self.volumeSelector.noneEnabled = False
        self.volumeSelector.removeEnabled = False
        self.volumeSelector.showHidden = False
        self.volumeSelector.showChildNodeTypes = False
        self.volumeSelector.setMRMLScene(slicer.mrmlScene)
        self.volumeSelector.setMinimumWidth(150)
        self.volumeSelector.setStyleSheet("margin: 15px 0")
        # self.volumeSelector.selectNodeUponCreation = False
        self.mainLayout.addWidget(self.volumeSelector, 0, 1, 1, 2)
        self.volumeSelector.connect('currentNodeChanged(vtkMRMLNode*)',
                                    self._onMainVolumeChanged_)

        ### Radio buttons frame
        self.radioButtonsFrame = qt.QFrame()
        self.radioButtonsLayout = qt.QHBoxLayout(self.radioButtonsFrame)
        self.typesFrame = qt.QFrame()
        self.radioButtonsLayout.addWidget(self.typesFrame)
        self.typesLayout = qt.QVBoxLayout(self.typesFrame)

        labelsStyle = "font-weight: bold; margin: 0 0 10px 0px;"
        # Types Radio Buttons
        typesLabel = qt.QLabel("Select type")
        typesLabel.setStyleSheet(labelsStyle)
        self.typesLayout.addWidget(typesLabel)
        self.typesRadioButtonGroup = qt.QButtonGroup()
        for key in self.logic.params.mainTypes.keys():
            rbitem = qt.QRadioButton(self.logic.params.getMainTypeLabel(key))
            self.typesRadioButtonGroup.addButton(rbitem, key)
            self.typesLayout.addWidget(rbitem)
        self.typesRadioButtonGroup.buttons()[0].setChecked(True)

        # Subtypes Radio buttons
        # The content will be loaded dynamically every time the main type is modified
        self.subtypesFrame = qt.QFrame()
        self.radioButtonsLayout.addWidget(self.subtypesFrame)
        self.subtypesLayout = qt.QVBoxLayout(self.subtypesFrame)
        subtypesLabel = qt.QLabel("Select subtype")
        subtypesLabel.setStyleSheet(labelsStyle)
        self.subtypesLayout.addWidget(subtypesLabel)
        self.subtypesLayout.setAlignment(SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.subtypesRadioButtonGroup = qt.QButtonGroup()
        # Add all the subtypes (we will filter later in "updateState" function)
        for key in self.logic.params.subtypes.keys():
            # Build the description
            rbitem = qt.QRadioButton(self.logic.params.getSubtypeLabel(key))
            self.subtypesRadioButtonGroup.addButton(rbitem, key)
            self.subtypesLayout.addWidget(rbitem,
                                          SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.subtypesLayout.addStretch()

        # Region radio buttons
        self.regionsFrame = qt.QFrame()
        self.radioButtonsLayout.addWidget(self.regionsFrame)
        self.regionsLayout = qt.QVBoxLayout(self.regionsFrame)
        regionsLabel = qt.QLabel("Select region")
        regionsLabel.setStyleSheet(labelsStyle)
        self.regionsLayout.addWidget(regionsLabel)
        self.regionsLayout.setAlignment(SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.regionsLayout.setStretch(0, 0)
        self.regionsRadioButtonGroup = qt.QButtonGroup()
        self.regionsFrame = qt.QFrame()
        # Add all the regions
        for key in self.logic.params.regions.keys():
            # Build the description
            rbitem = qt.QRadioButton(self.logic.params.getRegionLabel(key))
            self.regionsRadioButtonGroup.addButton(rbitem, key)
            self.regionsLayout.addWidget(rbitem,
                                         SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.regionsLayout.addStretch()
        self.regionsRadioButtonGroup.buttons()[0].setChecked(True)

        # Artifact radio buttons (Add them to the same layout as the type)
        # self.separatorLabel = qt.QLabel("------------")
        # labelsStyle = "margin: 5px 0 5px 0;"
        # self.separatorLabel.setStyleSheet(labelsStyle)
        # self.typesLayout.addWidget(self.separatorLabel)
        # self.artifactsLabel = qt.QLabel("Select artifact")
        # labelsStyle = "font-weight: bold; margin: 15px 0 10px 0;"
        # self.artifactsLabel.setStyleSheet(labelsStyle)
        # self.typesLayout.addWidget(self.artifactsLabel)
        # self.artifactsRadioButtonGroup = qt.QButtonGroup()
        # for artifactId in self.logic.params.artifacts.iterkeys():
        #     rbitem = qt.QRadioButton(self.logic.params.getArtifactLabel(artifactId))
        #     self.artifactsRadioButtonGroup.addButton(rbitem, artifactId)
        #     self.typesLayout.addWidget(rbitem)
        # self.artifactsRadioButtonGroup.buttons()[0].setChecked(True)
        #
        self.typesLayout.setAlignment(SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.typesLayout.addStretch()

        # Connections
        self.typesRadioButtonGroup.connect("buttonClicked (QAbstractButton*)",
                                           self.__onTypesRadioButtonClicked__)
        self.subtypesRadioButtonGroup.connect(
            "buttonClicked (QAbstractButton*)",
            self.__onSecondaryRadioButtonClicked__)
        self.regionsRadioButtonGroup.connect(
            "buttonClicked (QAbstractButton*)",
            self.__onSecondaryRadioButtonClicked__)
        # self.artifactsRadioButtonGroup.connect("buttonClicked (QAbstractButton*)", self.__onTypesRadioButtonClicked__)

        self.mainLayout.addWidget(self.radioButtonsFrame, 2, 0, 1, 3,
                                  SlicerUtil.ALIGNMENT_VERTICAL_TOP)

        # Save results button
        self.saveResultsButton = ctk.ctkPushButton()
        self.saveResultsButton.setText("Save results")
        self.saveResultsButton.toolTip = "Save the results labelmap in the specified directory"
        self.saveResultsButton.setIcon(
            qt.QIcon("{0}/Save.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.saveResultsButton.setIconSize(qt.QSize(20, 20))
        self.saveResultsButton.setFixedWidth(135)
        self.mainLayout.addWidget(self.saveResultsButton, 4, 0)
        self.saveResultsButton.connect('clicked()',
                                       self._onSaveResultsButtonClicked_)

        # Save results directory button
        defaultPath = os.path.join(
            SlicerUtil.getSettingsDataFolder(self.moduleName),
            "results")  # Assign a default path for the results
        path = SlicerUtil.settingGetOrSetDefault(self.moduleName,
                                                 "SaveResultsDirectory",
                                                 defaultPath)
        self.saveResultsDirectoryButton = ctk.ctkDirectoryButton()
        self.saveResultsDirectoryButton.directory = path
        self.saveResultsDirectoryButton.setMaximumWidth(440)
        self.mainLayout.addWidget(self.saveResultsDirectoryButton, 4, 1, 1, 2)
        self.saveResultsDirectoryButton.connect(
            "directoryChanged (QString)", self._onSaveResultsDirectoryChanged_)

        self._createSegmentEditorWidget_()

        # MIP viewer (by default it will be hidden)
        self.mipCollapsibleButton = ctk.ctkCollapsibleButton()
        self.mipCollapsibleButton.text = "MIP viewer"
        mipLayout = qt.QVBoxLayout(self.mipCollapsibleButton)
        self.mainLayout.addWidget(self.mipCollapsibleButton)
        self.mipViewer = CIPUI.MIPViewerWidget(mipLayout)
        self.mipCollapsibleButton.setVisible(False)
        self.mipViewer.setup()
        self.mipViewer.isCrosshairEnabled = False
        self.mipCollapsibleButton.collapsed = True

        #####
        # Case navigator
        self.caseNavigatorWidget = None
        if SlicerUtil.isSlicerACILLoaded():
            caseNavigatorAreaCollapsibleButton = ctk.ctkCollapsibleButton()
            caseNavigatorAreaCollapsibleButton.text = "Case navigator"
            self.layout.addWidget(caseNavigatorAreaCollapsibleButton, 0x0020)
            # caseNavigatorLayout = qt.QVBoxLayout(caseNavigatorAreaCollapsibleButton)

            # Add a case list navigator
            from ACIL.ui import CaseNavigatorWidget
            self.caseNavigatorWidget = CaseNavigatorWidget(
                self.moduleName, caseNavigatorAreaCollapsibleButton)
            self.caseNavigatorWidget.setup()
            # Listen for event in order to save the current labelmap before moving to the next case
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_PRE_NEXT,
                self._checkSaveChanges_)
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_PRE_PREVIOUS,
                self._checkSaveChanges_)
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_PRE_LABELMAP_LOAD,
                self._onPreNavigatorLabelmapLoaded_)
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_LABELMAP_LOADED,
                self._onNavigatorLabelmapLoaded_)

        self.layout.addStretch()

        # Extra Connections
        self._createSceneObservers_()

        self.disableEvents = False
        self.setMainTypeGUIProperties()

    @property
    def currentVolumeLoaded(self):
        return self.volumeSelector.currentNode()

    def _initLogic_(self):
        """Create a new logic object for the plugin"""
        self.logic = CIP_ParenchymaSubtypeTrainingLabellingLogic()

    def _createSceneObservers_(self):
        """
        Create the observers for the scene in this module
        """
        self.observers = []
        # self.observers.append(
        #     slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, self.__onNodeAddedObserver__))
        self.observers.append(
            slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent,
                                         self.__onSceneClosed__))

    def saveResultsCurrentNode(self):
        """ Get current active node and save the xml fiducials file
        """
        try:
            d = self.saveResultsDirectoryButton.directory
            if not os.path.isdir(d):
                # Ask the user if he wants to create the folder
                if qt.QMessageBox.question(
                        slicer.util.mainWindow(), "Create directory?",
                        "The directory '{0}' does not exist. Do you want to create it?"
                        .format(d), qt.QMessageBox.Yes
                        | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                    try:
                        os.makedirs(d)
                        # Make sure that everybody has write permissions (sometimes there are problems because of umask)
                        os.chmod(d, 0o777)
                    except:
                        qt.QMessageBox.warning(
                            slicer.util.mainWindow(), 'Directory incorrect',
                            'The folder "{0}" could not be created. Please select a valid directory'
                            .format(d))
                        return
                else:
                    # Abort process
                    SlicerUtil.logDevelop("Saving results process aborted",
                                          includePythonConsole=True)
                    return
                    # self.logic.saveCurrentFiducials(d, self.caseNavigatorWidget, self.uploadFileResult)
                    # qt.QMessageBox.information(slicer.util.mainWindow(), 'Results saved',
                    #                            "The results have been saved succesfully")
            # else:
            if SlicerUtil.isSlicerACILLoaded():
                question = qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save results remotely?",
                    "Your results will be saved locally. Do you also want to save your results in your remote server? (MAD, etc.)",
                    qt.QMessageBox.Yes | qt.QMessageBox.No
                    | qt.QMessageBox.Cancel)
                if question == qt.QMessageBox.Cancel:
                    return
                saveInRemoteRepo = question == qt.QMessageBox.Yes
            else:
                saveInRemoteRepo = False
            self.logic.saveCurrentFiducials(
                d,
                caseNavigatorWidget=self.caseNavigatorWidget,
                callbackFunction=self.uploadFileResult,
                saveInRemoteRepo=saveInRemoteRepo)
            qt.QMessageBox.information(
                slicer.util.mainWindow(), 'Results saved',
                "The results have been saved succesfully")
        except:
            Util.print_last_exception()
            qt.QMessageBox.critical(
                slicer.util.mainWindow(), "Error when saving the results",
                "Error when saving the results. Please review the console for additional info"
            )

    def uploadFileResult(self, result):
        """Callback method that will be invoked by the CaseNavigator after uploading a file remotely"""
        if result != Util.OK:
            qt.QMessageBox.warning(
                slicer.util.mainWindow(), "Error when uploading fiducials",
                "There was an error when uploading the fiducials file. This doesn't mean that your file wasn't saved locally!\n"
                + "Please review the console for more information")

    @property
    def colorNode(self):
        nodeName = "{}_colorNode".format(self.moduleName)
        colorTableNode = SlicerUtil.getNode(nodeName)
        if colorTableNode is None:
            colorTableNode = self.logic.params.createColormapNode(nodeName)
        return colorTableNode

    def setMIPViewerVisible(self, show):
        self.mipCollapsibleButton.setVisible(show)

    def checkMasterAndLabelMapNodes(self):
        """Set an appropiate MasterNode LabelMapNode to the Editor.
        The options are:
            - There is no masterNode => try to load the one that the user is watching right now, and go on if so.
            - There is masterNode and there is no label map => create a default label map node with the name "MasterNodeName_structuresDetection" and set the StructuresDetectionColorMap
            - There is masterNode and there is label map => check if the name of the label map is "MasterNodeName_structuresDetection".
                - If so: set this one
                - Otherwise: create a new labelmap with the name 'MasterNodeName_structureslabelMap' """
        if self.disableEvents: return  # To avoid infinite loops

        if self.editorWidget.masterVolume:
            masterNode = self.editorWidget.masterVolume
            SlicerUtil.logDevelop(
                "Master node in Editor = " + masterNode.GetName(), True)
        else:
            SlicerUtil.logDevelop(
                "No master node in Editor. Retrieving it from the selector...",
                False)
            masterNode = self.getCurrentGrayscaleNode()

        if not masterNode:
            # There is no any volume node that the user is watching
            SlicerUtil.logDevelop("Still not master node. Exit", False)
            return

        labelmapNode = self.getOrCreateLabelmap(masterNode)
        displayNode = labelmapNode.GetDisplayNode()

        if displayNode:
            displayNode.SetAndObserveColorNodeID(self.colorNode.GetID())
        else:
            SlicerUtil.logDevelop(
                "There is no DisplayNode for label map " +
                labelmapNode.GetName(), True)

        slicer.app.applicationLogic().PropagateVolumeSelection(0)
        SlicerUtil.changeLabelmapOpacity(0.5)

        # Set the right volumes
        self.disableEvents = True
        #self.editorWidget.masterVolume = masterNode
        #self.editorWidget.labelmapVolume = labelmapNode
        # trigger editor events
        self.editorWidget.helper.setVolumes(masterNode, labelmapNode)
        self.disableEvents = False

        slicer.app.applicationLogic().FitSliceToAll()

    def getOrCreateLabelmap(self, masterNode):
        labelmapName = "{0}_{1}".format(masterNode.GetName(),
                                        self.labelmapNodeNameExtension)
        labelmapNode = SlicerUtil.getNode(labelmapName)
        if labelmapNode is None:
            # Create a labelmap for this scalar
            labelmapNode = slicer.modules.volumes.logic(
            ).CreateAndAddLabelVolume(slicer.mrmlScene, masterNode,
                                      labelmapName)
            # Make sure that the labelmap has this name (no suffixes)
            labelmapNode.SetName(labelmapName)
            # Register the labelmap in the case navigator so that it is removed when moving to another case
            if SlicerUtil.isSlicerACILLoaded():
                self.caseNavigatorWidget.registerVolumeId(labelmapNode.GetID())
            SlicerUtil.logDevelop("New label map node created: " +
                                  labelmapName,
                                  includePythonConsole=True)
        else:
            SlicerUtil.logDevelop("Labelmap loaded", includePythonConsole=True)
        return labelmapNode

    def getCurrentGrayscaleNode(self):
        """Get the grayscale node that is currently active in the widget"""
        #return self.editorWidget.masterVolume
        return self.volumeSelector.currentNode()

    def getCurrentLabelMapNode(self):
        """Get the labelmap node that is currently active in the widget"""
        return self.editorWidget.labelmapVolume

    def setCurrentGrayscaleNode(self, node):
        """Get the grayscale node that is currently active in the widget"""
        self.editorWidget.masterVolume = node

    def setCurrentLabelMapNode(self, node):
        """Get the labelmap node that is currently active in the widget"""
        self.editorWidget.labelmapVolume = node

    def setMainTypeGUIProperties(self):
        """Show/Hide the right Radio Buttons and set the right color for drawing based on the selected type-subtype"""
        # Load the subtypes for this type
        subtypesDict = self.logic.getSubtypes(
            self.typesRadioButtonGroup.checkedId())

        # Hide/Show the subtypes for this type
        for b in self.subtypesRadioButtonGroup.buttons():
            id = self.subtypesRadioButtonGroup.id(b)
            if id in subtypesDict:
                b.show()
            else:
                b.hide()

        # if self.artifactsRadioButtonGroup.checkedId() == 0:
        # No artifact. Check first element by default
        self.subtypesRadioButtonGroup.buttons()[0].setChecked(True)

        # Set the right color in the colormap
        typeId = self.typesRadioButtonGroup.checkedId()
        regionId = self.regionsRadioButtonGroup.checkedId()
        colorId = (typeId << 8) + regionId
        self.editorWidget.toolsColor.colorSpin.setValue(colorId)
        self.editorWidget.setActiveEffect("PaintEffect")

    def setSecondaryTypeGUIProperties(self):
        subtype = self.subtypesRadioButtonGroup.checkedId()
        t = self.typesRadioButtonGroup.checkedId(
        ) if subtype == 0 else self.subtypesRadioButtonGroup.checkedId()
        # if subtype == 0:
        #     # Subtype "Any". Select main type
        #     self.editorWidget.toolsColor.colorSpin.setValue(self.typesRadioButtonGroup.checkedId())
        # else:
        #     # Select subtype
        #     self.editorWidget.toolsColor.colorSpin.setValue(self.subtypesRadioButtonGroup.checkedId())
        regionId = self.regionsRadioButtonGroup.checkedId()
        colorId = (t << 8) + regionId
        self.editorWidget.toolsColor.colorSpin.setValue(colorId)
        self.editorWidget.setActiveEffect("PaintEffect")

    def saveResults(self):
        try:
            if SlicerUtil.isSlicerACILLoaded():
                saveResultsRemotely = qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save volume remotely?",
                    "Do you want to save the results remotely?",
                    qt.QMessageBox.Yes
                    | qt.QMessageBox.No) == qt.QMessageBox.Yes
            else:
                saveResultsRemotely = False
            # First, save locally to the results directory (as a backup)
            labelmap = self.getCurrentLabelMapNode()
            localPath = os.path.join(self.saveResultsDirectoryButton.directory,
                                     labelmap.GetName() + ".nrrd")
            if saveResultsRemotely:
                self.caseNavigatorWidget.uploadVolume(
                    labelmap,
                    callbackFunction=self._uploadFileCallback_,
                    localPath=localPath)
            else:
                slicer.util.saveNode(labelmap, localPath)
                slicer.util.infoDisplay(
                    "Results saved to '{}'".format(localPath))

        except Exception as ex:
            Util.print_last_exception()
            slicer.util.errorDisplay(ex.message)

    ##############
    # Aux methods
    ##############
    def _onMainVolumeChanged_(self, newVolumeNode):
        """ A volume was changed in the main volume selector
        :param newVolumeNode:
        :return:
        """
        if not self.disableEvents:
            self.setCurrentGrayscaleNode(newVolumeNode)
            self.checkMasterAndLabelMapNodes()

    def _onPreNavigatorLabelmapLoaded_(self, volumeNodeName):
        self.labelmapToBeRemoved = SlicerUtil.getNode(volumeNodeName)

    def _onNavigatorLabelmapLoaded_(self, volumeNode, region, type):
        """When a labelmap is loaded in the CaseNavigator, remove possible preexisting nodes"""
        if self.labelmapToBeRemoved:
            slicer.mrmlScene.RemoveNode(self.labelmapToBeRemoved)
            self.labelmapToBeRemoved = None

        self.checkMasterAndLabelMapNodes()

    def _createSegmentEditorWidget_(self):
        """Create and initialize a customize Slicer Editor which contains just some the tools that we need for the segmentation"""

        import qSlicerSegmentationsModuleWidgetsPythonQt
        self.segmentEditorWidget = qSlicerSegmentationsModuleWidgetsPythonQt.qMRMLSegmentEditorWidget(
        )
        self.segmentEditorWidget.setMaximumNumberOfUndoStates(10)
        self.segmentEditorWidget.setMRMLScene(slicer.mrmlScene)
        self.segmentEditorWidget.unorderedEffectsVisible = False
        self.segmentEditorWidget.setEffectNameOrder([
            'Paint', 'Draw', 'Erase', 'Threshold', 'Grow from seeds',
            'Scissors'
        ])
        self.layout.addWidget(self.segmentEditorWidget)

        # Collapse Volumes selector by default
        #self.editorWidget.volumes.collapsed = True

        # Remove current listeners for helper box and override them
        #self.editorWidget.helper.masterSelector.disconnect("currentNodeChanged(vtkMRMLNode*)")
        #self.editorWidget.helper.mergeSelector.disconnect("currentNodeChanged(vtkMRMLNode*)")
        # Force to select always a node. It is important to do this at this point, when the events are disconnected,
        # because otherwise the editor would display the color selector (just noisy for the user)
        #self.editorWidget.helper.masterSelector.noneEnabled = False
        # Listen to the event when there is a Master Node selected in the HelperBox
        #self.editorWidget.helper.masterSelector.connect("currentNodeChanged(vtkMRMLNode*)", self._onMasterNodeSelect_)

    def _collapseEditorWidget_(self, collapsed=True):
        """Collapse/expand the items in EditorWidget"""
        self.editorWidget.volumes.collapsed = collapsed
        self.editorWidget.editLabelMapsFrame.collapsed = collapsed

    def _uploadFileCallback_(self, result):
        """
        Callback after uploading a file (xml/labelmap) to the remote server
        @param result:
        """
        if result == Util.OK:
            qt.QMessageBox.information(slicer.util.mainWindow(),
                                       "Results saved successfully",
                                       "Results saved successfully")
        else:
            qt.QMessageBox.warning(
                slicer.util.mainWindow(), "Error when uploading the data",
                "There was an error when uploading the results file to the remote server.\n "
                "This doesn't mean that your file wasn't saved locally!\n"
                "Please review the console for more information")

    def _checkSaveChanges_(self):
        """ New volume loaded in the scene in some way.
        If it's really a new volume, try to save and close the current one
        @param newVolumeNode:
        """
        if self.currentVolumeLoaded is not None:
            # Ask the user if he wants to save the previously loaded volume
            if qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save results?",
                    "Do you want to save changes for volume {0}?".format(
                        self.currentVolumeLoaded.GetName()), qt.QMessageBox.Yes
                    | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                self.saveResults()

    def _onClearLabelmapButtonClicked_(self):
        if qt.QMessageBox.question(
                slicer.util.mainWindow(), "Clear labelmap?",
                "Are you sure you want to clear the current labelmap? (THIS OPERATION CANNOT BE UNDONE)",
                qt.QMessageBox.Yes | qt.QMessageBox.No) == qt.QMessageBox.Yes:
            SlicerUtil.clearVolume(self.getCurrentLabelMapNode())

    #########
    # Events
    #########
    def enter(self):
        """Method that is invoked when we switch to the module in slicer user interface"""
        self.disableEvents = False
        if self.firstLoad:
            self.firstLoad = False
        else:
            self.checkMasterAndLabelMapNodes()
            self.editorWidget.helper.masterSelector.connect(
                "currentNodeChanged(vtkMRMLNode*)", self._onMasterNodeSelect_)

    def _onMasterNodeSelect_(self, node):
        if node:
            nodeName = node.GetName()
            if self.getCurrentGrayscaleNode(
            ) and self.getCurrentGrayscaleNode().GetName() != nodeName:
                SlicerUtil.logDevelop(
                    "There was a selection of a new master node: {0}. Previous: {1}. We will invoke checkMasterAndLabelMapNodes"
                    .format(node.GetName(),
                            self.editorWidget.masterVolume.GetName()),
                    includePythonConsole=True)
                # Update Editor Master node to perform the needed actions.
                # We don't use "setVolumes" function because the interface must not be refeshed yet (it will be in checkMasterAndLabelMapNodes)
                self.setCurrentGrayscaleNode(node)
                # Remove label node to refresh the values properly
                self.setCurrentLabelMapNode(None)
                self.checkMasterAndLabelMapNodes()
        else:
            SlicerUtil.logDevelop(
                "No master node selected. Trying to remove label map", False)
            self.editorWidget.cleanVolumes()
        self.setMainTypeGUIProperties()

    def __onTypesRadioButtonClicked__(self, button):
        """ One of the radio buttons has been pressed
        :param button:
        :return:
        """
        self.setMainTypeGUIProperties()

    def __onSecondaryRadioButtonClicked__(self, button):
        """ One of the subtype radio buttons has been pressed
        :param button:
        :return:
        """
        self.setSecondaryTypeGUIProperties()

    def _onSaveResultsDirectoryChanged_(self, directory):
        SlicerUtil.setSetting(self.moduleName, "SaveResultsDirectory",
                              directory)

    def _onSaveResultsButtonClicked_(self):
        self.saveResults()

    def __onNavigatorCaseChange__(self):
        self._checkSaveChanges_()

    def __onSceneClosed__(self, arg1, arg2):
        self.pendingChangesIdsList = []
        self.logic = CIP_ParenchymaSubtypeTrainingLabellingLogic()

    def exit(self):
        self.editorWidget.helper.masterSelector.disconnect(
            "currentNodeChanged(vtkMRMLNode*)")
        self.disableEvents = True

    def cleanup(self):
        pass
Exemplo n.º 3
0
class CIP_PointsLabellingWidget(ScriptedLoadableModuleWidget):
    """Uses ScriptedLoadableModuleWidget base class, available at:
    https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py
    """
    @property
    def moduleName(self):
        # TODO: get it from file name
        return "CIP_PointsLabelling"

    def __init__(self, parent):
        ScriptedLoadableModuleWidget.__init__(self, parent)

        from functools import partial

        def __onNodeAddedObserver__(self, caller, eventId, callData):
            """Node added to the Slicer scene"""
            if callData.GetClassName() == 'vtkMRMLScalarVolumeNode' \
                    and slicer.util.mainWindow().moduleSelector().selectedModule == self.moduleName:
                self.__onNewVolumeLoaded__(callData)
            # elif callData.GetClassName() == 'vtkMRMLLabelMapVolumeNode':
            #     self.__onNewLabelmapLoaded__(callData)

        self.__onNodeAddedObserver__ = partial(__onNodeAddedObserver__, self)
        self.__onNodeAddedObserver__.CallDataType = vtk.VTK_OBJECT

    def _initLogic_(self):
        """Create a new logic object for the plugin"""
        self.logic = CIP_PointsLabellingLogic()

    def setup(self):
        """This is called one time when the module GUI is initialized
        """
        ScriptedLoadableModuleWidget.setup(self)

        # Create objects that can be used anywhere in the module. Example: in most cases there should be just one
        # object of the logic class
        self._initLogic_()
        self.currentVolumeLoaded = None
        self.blockNodeEvents = False

        ##########
        # Main area
        self.mainAreaCollapsibleButton = ctk.ctkCollapsibleButton()
        self.mainAreaCollapsibleButton.text = "Main area"
        self.layout.addWidget(self.mainAreaCollapsibleButton,
                              SlicerUtil.ALIGNMENT_VERTICAL_TOP)
        self.mainLayout = qt.QGridLayout(self.mainAreaCollapsibleButton)

        # Node selector
        volumeLabel = qt.QLabel("Active volume: ")
        volumeLabel.setStyleSheet("margin-left:5px")
        self.mainLayout.addWidget(volumeLabel, 0, 0)
        self.volumeSelector = slicer.qMRMLNodeComboBox()
        self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "")
        self.volumeSelector.selectNodeUponCreation = True
        self.volumeSelector.autoFillBackground = True
        self.volumeSelector.addEnabled = False
        self.volumeSelector.noneEnabled = False
        self.volumeSelector.removeEnabled = False
        self.volumeSelector.showHidden = False
        self.volumeSelector.showChildNodeTypes = False
        self.volumeSelector.setMRMLScene(slicer.mrmlScene)
        self.volumeSelector.setFixedWidth(250)
        self.volumeSelector.setStyleSheet("margin: 15px 0")
        #self.volumeSelector.selectNodeUponCreation = False
        self.mainLayout.addWidget(self.volumeSelector, 0, 1, 1, 3)
        self.volumeSelector.connect('currentNodeChanged(vtkMRMLNode*)',
                                    self.__onCurrentNodeChanged__)

        # Radio buttons frame. This will be filled by every child module
        self.radioButtonsFrame = qt.QFrame()
        self.mainLayout.addWidget(self.radioButtonsFrame, 2, 0, 1, 3,
                                  SlicerUtil.ALIGNMENT_VERTICAL_TOP)

        # Load caselist button
        self.loadButton = ctk.ctkPushButton()
        self.loadButton.text = "Load fiducials file"
        self.loadButton.setIcon(
            qt.QIcon("{0}/open_file.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.loadButton.setIconSize(qt.QSize(20, 20))
        self.loadButton.setFixedWidth(135)
        self.mainLayout.addWidget(self.loadButton, 3, 0)
        self.loadButton.connect('clicked()', self.openFiducialsFile)

        # Remove fiducial button
        self.removeLastFiducialButton = ctk.ctkPushButton()
        self.removeLastFiducialButton.text = "Remove last fiducial"
        self.removeLastFiducialButton.toolTip = "Remove the last fiducial added"
        self.removeLastFiducialButton.setIcon(
            qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.removeLastFiducialButton.setIconSize(qt.QSize(20, 20))
        self.removeLastFiducialButton.setFixedWidth(200)
        self.mainLayout.addWidget(self.removeLastFiducialButton, 3, 1)
        self.removeLastFiducialButton.connect(
            'clicked()', self.__onRemoveLastFiducialButtonClicked__)

        # Save results button
        self.saveResultsButton = ctk.ctkPushButton()
        self.saveResultsButton.setText("Save markups")
        self.saveResultsButton.toolTip = "Save the markups in the specified directory"
        self.saveResultsButton.setIcon(
            qt.QIcon("{0}/Save.png".format(SlicerUtil.CIP_ICON_DIR)))
        self.saveResultsButton.setIconSize(qt.QSize(20, 20))
        self.saveResultsButton.setFixedWidth(135)
        self.mainLayout.addWidget(self.saveResultsButton, 4, 0)
        self.saveResultsButton.connect('clicked()',
                                       self.__onSaveResultsButtonClicked__)

        # Save results directory button
        defaultPath = os.path.join(
            SlicerUtil.getSettingsDataFolder(self.moduleName),
            "results")  # Assign a default path for the results
        path = SlicerUtil.settingGetOrSetDefault(self.moduleName,
                                                 "SaveResultsDirectory",
                                                 defaultPath)
        self.saveResultsDirectoryButton = ctk.ctkDirectoryButton()
        self.saveResultsDirectoryButton.directory = path
        self.saveResultsDirectoryButton.setMaximumWidth(375)
        self.mainLayout.addWidget(self.saveResultsDirectoryButton, 4, 1, 1, 2)
        self.saveResultsDirectoryButton.connect(
            "directoryChanged (QString)",
            self.__onSaveResultsDirectoryChanged__)

        #####
        # Case navigator
        self.caseNavigatorWidget = None
        if SlicerUtil.isSlicerACILLoaded():
            caseNavigatorAreaCollapsibleButton = ctk.ctkCollapsibleButton()
            caseNavigatorAreaCollapsibleButton.text = "Case navigator"
            self.layout.addWidget(caseNavigatorAreaCollapsibleButton, 0x0020)
            # caseNavigatorLayout = qt.QVBoxLayout(caseNavigatorAreaCollapsibleButton)

            # Add a case list navigator
            from ACIL.ui import CaseNavigatorWidget
            self.caseNavigatorWidget = CaseNavigatorWidget(
                self.moduleName, caseNavigatorAreaCollapsibleButton)
            self.caseNavigatorWidget.setup()
            # Listen for the event of loading a new labelmap
            # self.caseNavigatorWidget.addObservable(self.caseNavigatorWidget.EVENT_LABELMAP_LOADED, self.__onNewILDClassificationLabelmapLoaded__)
            self.caseNavigatorWidget.addObservable(
                self.caseNavigatorWidget.EVENT_BUNDLE_CASE_FINISHED,
                self.__onFinishCaseBundleLoad__)

        self.layout.addStretch()

        # Extra Connections
        self._createSceneObservers_()

    def _createSceneObservers_(self):
        """
        Create the observers for the scene in this module
        """
        self.observers = []
        self.observers.append(
            slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent,
                                         self.__onNodeAddedObserver__))
        self.observers.append(
            slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent,
                                         self.__onSceneClosed__))

    def saveResultsCurrentNode(self):
        """ Get current active node and save the xml fiducials file
        """
        try:
            d = self.saveResultsDirectoryButton.directory
            if not os.path.isdir(d):
                # Ask the user if he wants to create the folder
                if qt.QMessageBox.question(
                        slicer.util.mainWindow(), "Create directory?",
                        "The directory '{0}' does not exist. Do you want to create it?"
                        .format(d), qt.QMessageBox.Yes
                        | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                    try:
                        os.makedirs(d)
                        # Make sure that everybody has write permissions (sometimes there are problems because of umask)
                        os.chmod(d, 0777)
                    except:
                        qt.QMessageBox.warning(
                            slicer.util.mainWindow(), 'Directory incorrect',
                            'The folder "{0}" could not be created. Please select a valid directory'
                            .format(d))
                        return
                else:
                    # Abort process
                    SlicerUtil.logDevelop("Saving results process aborted",
                                          includePythonConsole=True)
                    return
                    # self.logic.saveCurrentFiducials(d, self.caseNavigatorWidget, self.uploadFileResult)
                    # qt.QMessageBox.information(slicer.util.mainWindow(), 'Results saved',
                    #                            "The results have been saved succesfully")
            # else:
            if SlicerUtil.isSlicerACILLoaded():
                question = qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save results remotely?",
                    "Your results will be saved locally. Do you also want to save your results in your remote server? (MAD, etc.)",
                    qt.QMessageBox.Yes | qt.QMessageBox.No
                    | qt.QMessageBox.Cancel)
                if question == qt.QMessageBox.Cancel:
                    return
                saveInRemoteRepo = question == qt.QMessageBox.Yes
            else:
                saveInRemoteRepo = False
            self.logic.saveCurrentFiducials(
                d,
                caseNavigatorWidget=self.caseNavigatorWidget,
                callbackFunction=self.uploadFileResult,
                saveInRemoteRepo=saveInRemoteRepo)
            qt.QMessageBox.information(
                slicer.util.mainWindow(), 'Results saved',
                "The results have been saved succesfully")
        except:
            Util.print_last_exception()
            qt.QMessageBox.critical(
                slicer.util.mainWindow(), "Error when saving the results",
                "Error when saving the results. Please review the console for additional info"
            )

    def uploadFileResult(self, result):
        if result != Util.OK:
            qt.QMessageBox.warning(
                slicer.util.mainWindow(), "Error when uploading fiducials",
                "There was an error when uploading the fiducials file. This doesn't mean that your file wasn't saved locally!\n"
                + "Please review the console for more information")

    def openFiducialsFile(self):
        volumeNode = self.volumeSelector.currentNode()
        if volumeNode is None:
            qt.QMessageBox.warning(slicer.util.mainWindow(), 'Select a volume',
                                   'Please load a volume first')
            return

        f = qt.QFileDialog.getOpenFileName()
        if f:
            self.logic.loadFiducialsXml(volumeNode, f)
            self.saveResultsDirectoryButton.directory = os.path.dirname(f)
            qt.QMessageBox.information(slicer.util.mainWindow(), "File loaded",
                                       "File loaded successfully")

    ## PROTECTED/PRIVATE METHODS
    def _checkNewVolume_(self, newVolumeNode):
        """ New volume loaded in the scene in some way.
        If it's really a new volume, try to save and close the current one
        @param newVolumeNode:
        """
        if self.blockNodeEvents:
            # "Semaphore" to avoid duplicated events
            return

        self.blockNodeEvents = True
        volume = self.currentVolumeLoaded
        if volume is not None and newVolumeNode is not None \
                and newVolumeNode.GetID() != volume.GetID()  \
                and not self.logic.isVolumeSaved(volume.GetName()):
            # Ask the user if he wants to save the previously loaded volume
            if qt.QMessageBox.question(
                    slicer.util.mainWindow(), "Save results?",
                    "The fiducials for the volume '{0}' have not been saved. Do you want to save them?"
                    .format(volume.GetName()), qt.QMessageBox.Yes
                    | qt.QMessageBox.No) == qt.QMessageBox.Yes:
                self.saveResultsCurrentNode()
        # Remove all the previously existing nodes
        if self.currentVolumeLoaded is not None and newVolumeNode != self.currentVolumeLoaded:
            # Remove previously existing node
            self.logic.removeMarkupsAndNode(self.currentVolumeLoaded)

        if newVolumeNode is not None:
            SlicerUtil.setActiveVolumeIds(newVolumeNode.GetID())
            SlicerUtil.setFiducialsCursorMode(True, True)

        self.currentVolumeLoaded = newVolumeNode
        self.updateState()
        self.blockNodeEvents = False

    def _getColorTable_(self):
        """ Color table for this module for a better labelmap visualization.
        This must be implemented by child classes"""
        raise NotImplementedError(
            "This method should be implemented by child classes")

    ## EVENTS
    def enter(self):
        """This is invoked every time that we select this module as the active module in Slicer (not only the first time)"""
        self.blockNodeEvents = False
        # if len(self.observers) == 0:
        #     self.observers.append(slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, self.__onNodeAddedObserver__))
        #     self.observers.append(slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent, self.__onSceneClosed__))
        SlicerUtil.setFiducialsCursorMode(True, True)

        if self.volumeSelector.currentNodeId != "":
            SlicerUtil.setActiveVolumeIds(self.volumeSelector.currentNodeId)
            self.currentVolumeLoaded = slicer.mrmlScene.GetNodeByID(
                self.volumeSelector.currentNodeId)
            self.updateState()

    def exit(self):
        """This is invoked every time that we switch to another module (not only when Slicer is closed)."""
        try:
            self.blockNodeEvents = True
            SlicerUtil.setFiducialsCursorMode(False)
        except:
            pass

    def cleanup(self):
        """This is invoked as a destructor of the GUI when the module is no longer going to be used"""
        try:
            for observer in self.observers:
                slicer.mrmlScene.RemoveObserver(observer)
                self.observers.remove(observer)
        except:
            pass

    def __onNewVolumeLoaded__(self, newVolumeNode):
        """ Added a new node in the scene
        :param newVolumeNode:
        :return:
        """
        # Filter the name of the volume to remove possible suffixes added by Slicer
        filteredName = SlicerUtil.filterVolumeName(newVolumeNode.GetName())
        newVolumeNode.SetName(filteredName)
        self._checkNewVolume_(newVolumeNode)
        self.blockNodeEvents = True
        self.volumeSelector.setCurrentNode(newVolumeNode)
        self.blockNodeEvents = False

    def __onCurrentNodeChanged__(self, volumeNode):
        self._checkNewVolume_(volumeNode)

    def __onFinishCaseBundleLoad__(self, result, id, ids, additionalFilePaths):
        """
        Event triggered after a volume and the additional files have been loaded for a case.
        In this case, it is important to load a previously existing xml file
        @param result:
        @param id:
        @param ids:
        @param additionalFilePaths:
        @return:
        """
        if result == Util.OK and additionalFilePaths and os.path.exists(
                additionalFilePaths[0]):
            # Try to load a previously existing fiducials file downloaded with the ACIL case navigator
            self.logic.loadFiducialsXml(slicer.util.getNode(id),
                                        additionalFilePaths[0])

    def __onRemoveLastFiducialButtonClicked__(self):
        self.logic.removeLastFiducial()

    def __onSaveResultsDirectoryChanged__(self, directory):
        # f = qt.QFileDialog.getExistingDirectory()
        # if f:
        #     self.saveResultsDirectoryText.setText(f)
        SlicerUtil.setSetting(self.moduleName, "SaveResultsDirectory",
                              directory)

    def __onSaveResultsButtonClicked__(self):
        self.saveResultsCurrentNode()

    def __onSceneClosed__(self, arg1, arg2):
        self.currentVolumeLoaded = None
        self._initLogic_()