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_PAARatioLogic() # # Create all the widgets. Example Area mainAreaCollapsibleButton = ctk.ctkCollapsibleButton() mainAreaCollapsibleButton.text = "Main parameters" self.layout.addWidget(mainAreaCollapsibleButton) self.mainAreaLayout = qt.QGridLayout(mainAreaCollapsibleButton) self.label = qt.QLabel("Select the volume") self.label.setStyleSheet("margin:10px 0 20px 7px") self.mainAreaLayout.addWidget(self.label, 0, 0) self.volumeSelector = slicer.qMRMLNodeComboBox() self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "") self.volumeSelector.name = "paa_volumeSelector" self.volumeSelector.selectNodeUponCreation = True self.volumeSelector.autoFillBackground = True self.volumeSelector.addEnabled = True self.volumeSelector.noneEnabled = False self.volumeSelector.removeEnabled = False self.volumeSelector.showHidden = False self.volumeSelector.showChildNodeTypes = False self.volumeSelector.setMRMLScene(slicer.mrmlScene) self.volumeSelector.setStyleSheet( "margin:0px 0 0px 0; padding:2px 0 2px 5px") self.mainAreaLayout.addWidget(self.volumeSelector, 0, 1) self.jumptToTemptativeSliceButton = ctk.ctkPushButton() self.jumptToTemptativeSliceButton.name = "jumptToTemptativeSliceButton" self.jumptToTemptativeSliceButton.text = "Jump to temptative slice" self.jumptToTemptativeSliceButton.toolTip = "Jump to the best estimated slice to place the rulers" self.jumptToTemptativeSliceButton.setIcon( qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.jumptToTemptativeSliceButton.setIconSize(qt.QSize(20, 20)) self.jumptToTemptativeSliceButton.setStyleSheet("font-weight: bold;") # self.jumptToTemptativeSliceButton.setFixedWidth(140) self.mainAreaLayout.addWidget(self.jumptToTemptativeSliceButton, 1, 1) ### Structure Selector self.structuresGroupbox = qt.QGroupBox("Select the structure") self.groupboxLayout = qt.QVBoxLayout() self.structuresGroupbox.setLayout(self.groupboxLayout) self.mainAreaLayout.addWidget(self.structuresGroupbox, 2, 0) self.structuresButtonGroup = qt.QButtonGroup() # btn = qt.QRadioButton("None") # btn.visible = False # self.structuresButtonGroup.addButton(btn) # self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Both") btn.name = "paaButton" btn.checked = True self.structuresButtonGroup.addButton(btn, 0) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Pulmonary Arterial") btn.name = "paRadioButton" self.structuresButtonGroup.addButton(btn, 1) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Aorta") btn.name = "aortaRadioButton" self.structuresButtonGroup.addButton(btn, 2) self.groupboxLayout.addWidget(btn) ### Buttons toolbox self.buttonsToolboxFrame = qt.QFrame() self.buttonsToolboxLayout = qt.QGridLayout() self.buttonsToolboxFrame.setLayout(self.buttonsToolboxLayout) self.mainAreaLayout.addWidget(self.buttonsToolboxFrame, 2, 1) self.placeRulersButton = ctk.ctkPushButton() self.placeRulersButton.text = "Place ruler/s" self.placeRulersButton.name = "placeRulersButton" self.placeRulersButton.toolTip = "Place the ruler/s for the selected structure/s in the current slice" self.placeRulersButton.setIcon( qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeRulersButton.setIconSize(qt.QSize(20, 20)) self.placeRulersButton.setFixedWidth(105) self.placeRulersButton.setStyleSheet("font-weight:bold") self.buttonsToolboxLayout.addWidget(self.placeRulersButton, 0, 0) self.moveUpButton = ctk.ctkPushButton() self.moveUpButton.text = "Move up" self.moveUpButton.toolTip = "Move the selected ruler/s one slice up" self.moveUpButton.setIcon( qt.QIcon("{0}/move_up.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveUpButton.setIconSize(qt.QSize(20, 20)) self.moveUpButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveUpButton, 0, 1) self.moveDownButton = ctk.ctkPushButton() self.moveDownButton.text = "Move down" self.moveDownButton.toolTip = "Move the selected ruler/s one slice down" self.moveDownButton.setIcon( qt.QIcon("{0}/move_down.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveDownButton.setIconSize(qt.QSize(20, 20)) self.moveDownButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveDownButton, 0, 2) self.removeButton = ctk.ctkPushButton() self.removeButton.text = "Remove ALL rulers" self.removeButton.toolTip = "Remove all the rulers for this volume" self.removeButton.setIcon( qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR))) self.removeButton.setIconSize(qt.QSize(20, 20)) self.buttonsToolboxLayout.addWidget(self.removeButton, 1, 1, 1, 2, 2) ### Textboxes self.textboxesFrame = qt.QFrame() self.textboxesLayout = qt.QFormLayout() self.textboxesFrame.setLayout(self.textboxesLayout) self.textboxesFrame.setFixedWidth(190) self.mainAreaLayout.addWidget(self.textboxesFrame, 3, 0) self.paTextBox = qt.QLineEdit() self.paTextBox.setReadOnly(True) self.textboxesLayout.addRow("PA (mm): ", self.paTextBox) self.aortaTextBox = qt.QLineEdit() self.aortaTextBox.setReadOnly(True) self.textboxesLayout.addRow("Aorta (mm): ", self.aortaTextBox) self.ratioTextBox = qt.QLineEdit() self.ratioTextBox.name = "ratioTextBox" self.ratioTextBox.setReadOnly(True) self.textboxesLayout.addRow("Ratio PA/A: ", self.ratioTextBox) # Save case data self.reportsCollapsibleButton = ctk.ctkCollapsibleButton() self.reportsCollapsibleButton.text = "Reporting" self.layout.addWidget(self.reportsCollapsibleButton) self.reportsLayout = qt.QHBoxLayout(self.reportsCollapsibleButton) self.storedColumnNames = [ "caseId", "paDiameterMm", "aortaDiameterMm", "pa1r", "pa1a", "pa1s", "pa2r", "pa2a", "pa2s", "a1r", "a1a", "a1s", "a2r", "a2a", "a2s" ] columns = CaseReportsWidget.getColumnKeysNormalizedDictionary( self.storedColumnNames) self.reportsWidget = CaseReportsWidget( self.moduleName, columns, parentWidget=self.reportsCollapsibleButton) self.reportsWidget.setup() # Init state self.resetModuleState() self.preventSavingState = False self.saveStateBeforeEnteringModule() self.preventSavingState = True self.switchToRedView() ##### # Case navigator 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() self.layout.addStretch() # Connections self.observers = [] self.volumeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onVolumeSelectorChanged) self.jumptToTemptativeSliceButton.connect( 'clicked()', self.onJumpToTemptativeSliceButtonClicked) self.placeRulersButton.connect('clicked()', self.onPlaceRulersClicked) self.moveUpButton.connect('clicked()', self.onMoveUpRulerClicked) self.moveDownButton.connect('clicked()', self.onMoveDownRulerClicked) self.removeButton.connect('clicked()', self.onRemoveRulerClicked) self.reportsWidget.addObservable( self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) # Init state self.resetModuleState() self.preventSavingState = False self.saveStateBeforeEnteringModule() self.preventSavingState = True
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()
class CIP_PAARatioWidget(ScriptedLoadableModuleWidget): """Uses ScriptedLoadableModuleWidget base class, available at: https://github.com/Slicer/Slicer/blob/master/Base/Python/slicer/ScriptedLoadableModule.py """ @property def currentVolumeId(self): return self.volumeSelector.currentNodeID def __init__(self, parent): ScriptedLoadableModuleWidget.__init__(self, parent) self.moduleName = "CIP_PAARatio" 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: # Current module visible self.volumeSelector.setCurrentNode(callData) SlicerUtil.changeContrastWindow(350, 40) 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_PAARatioLogic() # # Create all the widgets. Example Area mainAreaCollapsibleButton = ctk.ctkCollapsibleButton() mainAreaCollapsibleButton.text = "Main parameters" self.layout.addWidget(mainAreaCollapsibleButton) self.mainAreaLayout = qt.QGridLayout(mainAreaCollapsibleButton) self.label = qt.QLabel("Select the volume") self.label.setStyleSheet("margin:10px 0 20px 7px") self.mainAreaLayout.addWidget(self.label, 0, 0) self.volumeSelector = slicer.qMRMLNodeComboBox() self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "") self.volumeSelector.name = "paa_volumeSelector" self.volumeSelector.selectNodeUponCreation = True self.volumeSelector.autoFillBackground = True self.volumeSelector.addEnabled = True self.volumeSelector.noneEnabled = False self.volumeSelector.removeEnabled = False self.volumeSelector.showHidden = False self.volumeSelector.showChildNodeTypes = False self.volumeSelector.setMRMLScene(slicer.mrmlScene) self.volumeSelector.setStyleSheet( "margin:0px 0 0px 0; padding:2px 0 2px 5px") self.mainAreaLayout.addWidget(self.volumeSelector, 0, 1) self.jumptToTemptativeSliceButton = ctk.ctkPushButton() self.jumptToTemptativeSliceButton.name = "jumptToTemptativeSliceButton" self.jumptToTemptativeSliceButton.text = "Jump to temptative slice" self.jumptToTemptativeSliceButton.toolTip = "Jump to the best estimated slice to place the rulers" self.jumptToTemptativeSliceButton.setIcon( qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.jumptToTemptativeSliceButton.setIconSize(qt.QSize(20, 20)) self.jumptToTemptativeSliceButton.setStyleSheet("font-weight: bold;") # self.jumptToTemptativeSliceButton.setFixedWidth(140) self.mainAreaLayout.addWidget(self.jumptToTemptativeSliceButton, 1, 1) ### Structure Selector self.structuresGroupbox = qt.QGroupBox("Select the structure") self.groupboxLayout = qt.QVBoxLayout() self.structuresGroupbox.setLayout(self.groupboxLayout) self.mainAreaLayout.addWidget(self.structuresGroupbox, 2, 0) self.structuresButtonGroup = qt.QButtonGroup() # btn = qt.QRadioButton("None") # btn.visible = False # self.structuresButtonGroup.addButton(btn) # self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Both") btn.name = "paaButton" btn.checked = True self.structuresButtonGroup.addButton(btn, 0) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Pulmonary Arterial") btn.name = "paRadioButton" self.structuresButtonGroup.addButton(btn, 1) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Aorta") btn.name = "aortaRadioButton" self.structuresButtonGroup.addButton(btn, 2) self.groupboxLayout.addWidget(btn) ### Buttons toolbox self.buttonsToolboxFrame = qt.QFrame() self.buttonsToolboxLayout = qt.QGridLayout() self.buttonsToolboxFrame.setLayout(self.buttonsToolboxLayout) self.mainAreaLayout.addWidget(self.buttonsToolboxFrame, 2, 1) self.placeRulersButton = ctk.ctkPushButton() self.placeRulersButton.text = "Place ruler/s" self.placeRulersButton.name = "placeRulersButton" self.placeRulersButton.toolTip = "Place the ruler/s for the selected structure/s in the current slice" self.placeRulersButton.setIcon( qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeRulersButton.setIconSize(qt.QSize(20, 20)) self.placeRulersButton.setFixedWidth(105) self.placeRulersButton.setStyleSheet("font-weight:bold") self.buttonsToolboxLayout.addWidget(self.placeRulersButton, 0, 0) self.moveUpButton = ctk.ctkPushButton() self.moveUpButton.text = "Move up" self.moveUpButton.toolTip = "Move the selected ruler/s one slice up" self.moveUpButton.setIcon( qt.QIcon("{0}/move_up.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveUpButton.setIconSize(qt.QSize(20, 20)) self.moveUpButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveUpButton, 0, 1) self.moveDownButton = ctk.ctkPushButton() self.moveDownButton.text = "Move down" self.moveDownButton.toolTip = "Move the selected ruler/s one slice down" self.moveDownButton.setIcon( qt.QIcon("{0}/move_down.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveDownButton.setIconSize(qt.QSize(20, 20)) self.moveDownButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveDownButton, 0, 2) self.removeButton = ctk.ctkPushButton() self.removeButton.text = "Remove ALL rulers" self.removeButton.toolTip = "Remove all the rulers for this volume" self.removeButton.setIcon( qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR))) self.removeButton.setIconSize(qt.QSize(20, 20)) self.buttonsToolboxLayout.addWidget(self.removeButton, 1, 1, 1, 2, 2) ### Textboxes self.textboxesFrame = qt.QFrame() self.textboxesLayout = qt.QFormLayout() self.textboxesFrame.setLayout(self.textboxesLayout) self.textboxesFrame.setFixedWidth(190) self.mainAreaLayout.addWidget(self.textboxesFrame, 3, 0) self.paTextBox = qt.QLineEdit() self.paTextBox.setReadOnly(True) self.textboxesLayout.addRow("PA (mm): ", self.paTextBox) self.aortaTextBox = qt.QLineEdit() self.aortaTextBox.setReadOnly(True) self.textboxesLayout.addRow("Aorta (mm): ", self.aortaTextBox) self.ratioTextBox = qt.QLineEdit() self.ratioTextBox.name = "ratioTextBox" self.ratioTextBox.setReadOnly(True) self.textboxesLayout.addRow("Ratio PA/A: ", self.ratioTextBox) # Save case data self.reportsCollapsibleButton = ctk.ctkCollapsibleButton() self.reportsCollapsibleButton.text = "Reporting" self.layout.addWidget(self.reportsCollapsibleButton) self.reportsLayout = qt.QHBoxLayout(self.reportsCollapsibleButton) self.storedColumnNames = [ "caseId", "paDiameterMm", "aortaDiameterMm", "pa1r", "pa1a", "pa1s", "pa2r", "pa2a", "pa2s", "a1r", "a1a", "a1s", "a2r", "a2a", "a2s" ] columns = CaseReportsWidget.getColumnKeysNormalizedDictionary( self.storedColumnNames) self.reportsWidget = CaseReportsWidget( self.moduleName, columns, parentWidget=self.reportsCollapsibleButton) self.reportsWidget.setup() # Init state self.resetModuleState() self.preventSavingState = False self.saveStateBeforeEnteringModule() self.preventSavingState = True self.switchToRedView() ##### # Case navigator 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() self.layout.addStretch() # Connections self.observers = [] self.volumeSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onVolumeSelectorChanged) self.jumptToTemptativeSliceButton.connect( 'clicked()', self.onJumpToTemptativeSliceButtonClicked) self.placeRulersButton.connect('clicked()', self.onPlaceRulersClicked) self.moveUpButton.connect('clicked()', self.onMoveUpRulerClicked) self.moveDownButton.connect('clicked()', self.onMoveDownRulerClicked) self.removeButton.connect('clicked()', self.onRemoveRulerClicked) self.reportsWidget.addObservable( self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) # Init state self.resetModuleState() self.preventSavingState = False self.saveStateBeforeEnteringModule() self.preventSavingState = True def enter(self): """This is invoked every time that we select this module as the active module in Slicer (not only the first time)""" # activeVolumeId = SlicerUtil.getActiveVolumeIdInRedSlice() # if activeVolumeId is not None: # self.volumeSelector.setCurrentNodeID(activeVolumeId) # if activeVolumeId not in self.logic.currentVolumesLoaded: # self.placeDefaultRulers(activeVolumeId) # Save state self.saveStateBeforeEnteringModule() # Start listening again to scene events self.__addSceneObservables__() volumeId = self.volumeSelector.currentNodeID if volumeId: SlicerUtil.displayBackgroundVolume(volumeId) # Show the current rulers (if existing) self.logic.rulersVisible(volumeId, visible=True) # This module always works in Axial SlicerUtil.changeLayoutToAxial() self.changeToDefaultContrastLevel() def exit(self): """This is invoked every time that we switch to another module (not only when Slicer is closed).""" # Stop listening to Scene events self.__removeSceneObservables() # Hide rulers if self.currentVolumeId: self.logic.rulersVisible(self.currentVolumeId, False) # Load previous state self.restoreStateBeforeExitingModule() def cleanup(self): """This is invoked as a destructor of the GUI when the module is no longer going to be used""" self.__removeSceneObservables() self.reportsWidget.cleanup() self.reportsWidget = None def saveStateBeforeEnteringModule(self): """Save the state of the module regarding labelmap, etc. This state will be saved/loaded when exiting/entering the module """ if self.preventSavingState: # Avoid that the first time that the module loads, the state is saved twice self.preventSavingState = False return # Save existing layout self.savedLayout = None if slicer.app.layoutManager() is not None: self.savedLayout = slicer.app.layoutManager().layout # Get the active volume (it it exists) activeVolumeId = SlicerUtil.getFirstActiveVolumeId() if activeVolumeId is None: # Reset state self.resetModuleState() else: # There is a Volume loaded. Save state try: self.savedVolumeID = activeVolumeId displayNode = SlicerUtil.getNode( activeVolumeId).GetDisplayNode() self.savedContrastLevel = (displayNode.GetWindow(), displayNode.GetLevel()) # activeLabelmapId = SlicerUtil.getFirstActiveLabelmapId() # self.savedLabelmapID = activeLabelmapId # if activeLabelmapId is None: # self.savedLabelmapOpacity = None # else: # self.savedLabelmapOpacity = SlicerUtil.getLabelmapOpacity() # # Hide any labelmap # SlicerUtil.displayLabelmapVolume(None) except: Util.print_last_exception() # Not action really needed pass def restoreStateBeforeExitingModule(self): """Load the last state of the module when the user exited (labelmap, opacity, contrast window, etc.) """ try: if self.savedVolumeID: # There is a previously saved valid state. SlicerUtil.setActiveVolumeIds(self.savedVolumeID) SlicerUtil.changeContrastWindow(self.savedContrastLevel[0], self.savedContrastLevel[1]) # if self.savedLabelmapID: # print "Restoring active labelmap: " + self.savedLabelmapID # # There was a valid labelmap. Restore it # SlicerUtil.displayLabelmapVolume(self.savedLabelmapID) # # Restore previous opacity # SlicerUtil.changeLabelmapOpacity(self.savedLabelmapOpacity) # else: # # Hide labelmap # print "No labelmap saved. Hide all" # SlicerUtil.displayLabelmapVolume(None) # else: # # Hide labelmap # print "No volume saved. Hide labelmap" # SlicerUtil.displayLabelmapVolume(None) # Restore layout SlicerUtil.changeLayout(self.savedLayout) except: Util.print_last_exception() pass def resetModuleState(self): """ Reset all the module state variables """ self.savedVolumeID = None # Active grayscale volume ID self.savedLabelmapID = None # Active labelmap node ID self.savedLabelmapOpacity = None # Labelmap opacity self.savedContrastLevel = ( None, None ) # Contrast window/level that the user had when entering the module SlicerUtil.changeContrastWindow(350, 40) def changeToDefaultContrastLevel(self): # Preferred contrast SlicerUtil.changeContrastWindow(1000, 200) def jumpToTemptativeSlice(self, volumeId): """ Jump the red window to a predefined slice based on the size of the volume :param volumeId: """ # Get the default coordinates of the ruler aorta1, aorta2, pa1, pa2 = self.logic.getDefaultCoords(volumeId) # Set the display in the right slice self.moveRedWindowToSlice(aorta1[2]) redSliceNode = slicer.util.getFirstNodeByClassByName( "vtkMRMLSliceNode", "Red") factor = 0.5 newFOVx = redSliceNode.GetFieldOfView()[0] * factor newFOVy = redSliceNode.GetFieldOfView()[1] * factor newFOVz = redSliceNode.GetFieldOfView()[2] # Move the camera up to fix the view redSliceNode.SetXYZOrigin(0, 50, 0) # Update the FOV (zoom in) redSliceNode.SetFieldOfView(newFOVx, newFOVy, newFOVz) # Refresh the data in the viewer redSliceNode.UpdateMatrices() def placeDefaultRulers(self, volumeId): """ Set the Aorta and PA rulers to a default estimated position and jump to that slice :param volumeId: """ if not volumeId: return # Hide all the actual ruler nodes self.logic.hideAllRulers() # Remove the current rulers for this volume self.logic.removeRulers(volumeId) # Create the default rulers self.logic.createDefaultRulers(volumeId, self.onRulerUpdated) # Activate both structures self.structuresButtonGroup.buttons()[0].setChecked(True) # Jump to the slice where the rulers are self.jumpToTemptativeSlice(volumeId) # Place the rulers in the current slice self.placeRuler() # Add the current volume to the list of loaded volumes #self.logic.currentVolumesLoaded.add(volumeId) # Modify the zoom of the Red slice redSliceNode = slicer.util.getFirstNodeByClassByName( "vtkMRMLSliceNode", "Red") factor = 0.5 newFOVx = redSliceNode.GetFieldOfView()[0] * factor newFOVy = redSliceNode.GetFieldOfView()[1] * factor newFOVz = redSliceNode.GetFieldOfView()[2] redSliceNode.SetFieldOfView(newFOVx, newFOVy, newFOVz) # Move the camera up to fix the view redSliceNode.SetXYZOrigin(0, 50, 0) # Refresh the data in the viewer redSliceNode.UpdateMatrices() def placeRuler(self): """ Place one or the two rulers in the current visible slice in Red node """ volumeId = self.volumeSelector.currentNodeID if volumeId == '': self.showUnselectedVolumeWarningMessage() return selectedStructure = self.getCurrentSelectedStructure() if selectedStructure == self.logic.NONE: qt.QMessageBox.warning( slicer.util.mainWindow(), 'Review structure', 'Please select Pulmonary Arterial, Aorta or both to place the right ruler/s' ) return # Get the current slice currentSlice = self.getCurrentRedWindowSlice() if selectedStructure == self.logic.BOTH: structures = [self.logic.PA, self.logic.AORTA] else: structures = [selectedStructure] for structure in structures: self.logic.placeRulerInSlice(volumeId, structure, currentSlice, self.onRulerUpdated) self.refreshTextboxes() def getCurrentSelectedStructure(self): """ Get the current selected structure id :return: self.logic.AORTA or self.logic.PA """ selectedStructureText = self.structuresButtonGroup.checkedButton().text if selectedStructureText == "Aorta": return self.logic.AORTA elif selectedStructureText == "Pulmonary Arterial": return self.logic.PA elif selectedStructureText == "Both": return self.logic.BOTH return self.logic.NONE def stepSlice(self, offset): """ Move the selected structure one slice up or down :param offset: +1 or -1 :return: """ volumeId = self.volumeSelector.currentNodeID if volumeId == '': self.showUnselectedVolumeWarningMessage() return selectedStructure = self.getCurrentSelectedStructure() if selectedStructure == self.logic.NONE: self.showUnselectedStructureWarningMessage() return if selectedStructure == self.logic.BOTH: # Move both rulers self.logic.stepSlice(volumeId, self.logic.AORTA, offset) newSlice = self.logic.stepSlice(volumeId, self.logic.PA, offset) else: newSlice = self.logic.stepSlice(volumeId, selectedStructure, offset) self.moveRedWindowToSlice(newSlice) def removeRulers(self): """ Remove all the rulers related to the current volume node :return: """ self.logic.removeRulers(self.volumeSelector.currentNodeID) self.refreshTextboxes(reset=True) def getCurrentRedWindowSlice(self): """ Get the current slice (in RAS) of the Red window :return: """ redNodeSliceNode = slicer.app.layoutManager().sliceWidget( 'Red').sliceLogic().GetSliceNode() return redNodeSliceNode.GetSliceOffset() def moveRedWindowToSlice(self, newSlice): """ Moves the red display to the specified RAS slice :param newSlice: slice to jump (RAS format) :return: """ redNodeSliceNode = slicer.app.layoutManager().sliceWidget( 'Red').sliceLogic().GetSliceNode() redNodeSliceNode.JumpSlice(0, 0, newSlice) def refreshTextboxes(self, reset=False): """ Update the information of the textboxes that give information about the measurements """ self.aortaTextBox.setText("0") self.paTextBox.setText("0") self.ratioTextBox.setText("0") self.ratioTextBox.setStyleSheet( " QLineEdit { background-color: white; color: black}") volumeId = self.volumeSelector.currentNodeID # if volumeId not in self.logic.currentVolumesLoaded: # return if volumeId: self.logic.changeActiveRulersColor(volumeId, self.logic.defaultColor) aorta = None pa = None if not reset: rulerAorta, newAorta = self.logic.getRulerNodeForVolumeAndStructure( self.volumeSelector.currentNodeID, self.logic.AORTA, createIfNotExist=False) rulerPA, newPA = self.logic.getRulerNodeForVolumeAndStructure( self.volumeSelector.currentNodeID, self.logic.PA, createIfNotExist=False) if rulerAorta: aorta = rulerAorta.GetDistanceMeasurement() self.aortaTextBox.setText(str(aorta)) if rulerPA: pa = rulerPA.GetDistanceMeasurement() self.paTextBox.setText(str(pa)) if pa is not None and aorta is not None and aorta != 0: try: ratio = pa / aorta self.ratioTextBox.setText(str(ratio)) if ratio > 1.0: # Switch colors ("alarm") st = " QLineEdit {{ background-color: rgb({0}, {1}, {2}); color: white }}". \ format(int(self.logic.defaultWarningColor[0]*255), int(self.logic.defaultWarningColor[1]*255), int(self.logic.defaultWarningColor[2]*255)) self.ratioTextBox.setStyleSheet(st) self.logic.changeActiveRulersColor( volumeId, self.logic.defaultWarningColor) except Exception: Util.print_last_exception() def showUnselectedVolumeWarningMessage(self): qt.QMessageBox.warning(slicer.util.mainWindow(), 'Select a volume', 'Please select a volume') def showUnselectedStructureWarningMessage(self): qt.QMessageBox.warning( slicer.util.mainWindow(), 'Review structure', 'Please select Aorta, Pulmonary Arterial or Both to place the right ruler/s' ) def switchToRedView(self): """ Switch the layout to Red slice only :return: """ layoutManager = slicer.app.layoutManager() # Test the layout manager is not none in case the module is initialized without a main window # This happens for example in automatic tests if layoutManager is not None: layoutManager.setLayout(6) def __addSceneObservables__(self): self.observers.append( slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, self.__onNodeAddedObserver__)) self.observers.append( slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent, self.__onSceneClosed__)) def __removeSceneObservables(self): for observer in self.observers: slicer.mrmlScene.RemoveObserver(observer) self.observers.remove(observer) ######### # EVENTS def onVolumeSelectorChanged(self, node): #if node is not None and node.GetID() not in self.currentVolumesLoaded: # if node is not None: # # New node. Load default rulers # if node.GetID() not in self.logic.currentVolumesLoaded: # self.placeDefaultRulers(node.GetID()) logging.info("Volume selector node changed: {0}".format( '(None)' if node is None else node.GetName())) # Preferred contrast (TODO: set right level) SlicerUtil.changeContrastWindow(1144, 447) self.refreshTextboxes() def onStructureClicked(self, button): fiducialsNode = self.getFiducialsNode( self.volumeSelector.currentNodeID) if fiducialsNode is not None: self.__addRuler__(button.text, self.volumeSelector.currentNodeID) markupsLogic = slicer.modules.markups.logic() markupsLogic.SetActiveListID(fiducialsNode) applicationLogic = slicer.app.applicationLogic() selectionNode = applicationLogic.GetSelectionNode() selectionNode.SetReferenceActivePlaceNodeClassName( "vtkMRMLAnnotationRulerNode") interactionNode = applicationLogic.GetInteractionNode() interactionNode.SwitchToSinglePlaceMode() def onJumpToTemptativeSliceButtonClicked(self): volumeId = self.volumeSelector.currentNodeID if volumeId == '': self.showUnselectedVolumeWarningMessage() return #self.placeDefaultRulers(volumeId) self.jumpToTemptativeSlice(volumeId) def onRulerUpdated(self, node, event): self.refreshTextboxes() def onPlaceRulersClicked(self): self.placeRuler() def onMoveUpRulerClicked(self): self.stepSlice(1) def onMoveDownRulerClicked(self): self.stepSlice(-1) def onRemoveRulerClicked(self): if (qt.QMessageBox.question( slicer.util.mainWindow(), 'Remove rulers', 'Are you sure you want to remove all the rulers from this volume?', qt.QMessageBox.Yes | qt.QMessageBox.No)) == qt.QMessageBox.Yes: self.logic.removeRulers(self.volumeSelector.currentNodeID) self.refreshTextboxes() def onSaveReport(self): """ Save the current values in a persistent csv file :return: """ volumeId = self.volumeSelector.currentNodeID if volumeId: caseName = slicer.mrmlScene.GetNodeByID(volumeId).GetName() coords = [0, 0, 0, 0] pa1 = pa2 = a1 = a2 = None # PA rulerNode, newNode = self.logic.getRulerNodeForVolumeAndStructure( volumeId, self.logic.PA, createIfNotExist=False) if rulerNode: # Get current RAS coords rulerNode.GetPositionWorldCoordinates1(coords) pa1 = list(coords) rulerNode.GetPositionWorldCoordinates2(coords) pa2 = list(coords) # AORTA rulerNode, newNode = self.logic.getRulerNodeForVolumeAndStructure( volumeId, self.logic.AORTA, createIfNotExist=False) if rulerNode: rulerNode.GetPositionWorldCoordinates1(coords) a1 = list(coords) rulerNode.GetPositionWorldCoordinates2(coords) a2 = list(coords) self.reportsWidget.insertRow( caseId=caseName, paDiameterMm=self.paTextBox.text, aortaDiameterMm=self.aortaTextBox.text, pa1r=pa1[0] if pa1 is not None else '', pa1a=pa1[1] if pa1 is not None else '', pa1s=pa1[2] if pa1 is not None else '', pa2r=pa2[0] if pa2 is not None else '', pa2a=pa2[1] if pa2 is not None else '', pa2s=pa2[2] if pa2 is not None else '', a1r=a1[0] if a1 is not None else '', a1a=a1[1] if a1 is not None else '', a1s=a1[2] if a1 is not None else '', a2r=a2[0] if a2 is not None else '', a2a=a2[1] if a2 is not None else '', a2s=a2[2] if a2 is not None else '') qt.QMessageBox.information(slicer.util.mainWindow(), 'Data saved', 'The data were saved successfully') def __onSceneClosed__(self, arg1, arg2): """ Scene closed. Reset currently loaded volumes :param arg1: :param arg2: :return: """ #self.logic.currentVolumesLoaded.clear() self.logic.currentActiveVolumeId = None
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__))
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
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_()
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()
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_()
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_PAARatioLogic() # # Create all the widgets. Example Area mainAreaCollapsibleButton = ctk.ctkCollapsibleButton() mainAreaCollapsibleButton.text = "Main parameters" self.layout.addWidget(mainAreaCollapsibleButton) self.mainAreaLayout = qt.QGridLayout(mainAreaCollapsibleButton) self.label = qt.QLabel("Select the volume") self.label.setStyleSheet("margin:10px 0 20px 7px") self.mainAreaLayout.addWidget(self.label, 0, 0) self.volumeSelector = slicer.qMRMLNodeComboBox() self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "") # DEPRECATED. Now there is a new vtkMRMLLabelMapNode # self.volumeSelector.addAttribute( "vtkMRMLScalarVolumeNode", "LabelMap", "0" ) # No labelmaps self.volumeSelector.selectNodeUponCreation = True self.volumeSelector.autoFillBackground = True self.volumeSelector.addEnabled = True self.volumeSelector.noneEnabled = False self.volumeSelector.removeEnabled = False self.volumeSelector.showHidden = False self.volumeSelector.showChildNodeTypes = False self.volumeSelector.setMRMLScene(slicer.mrmlScene) self.volumeSelector.setStyleSheet("margin:0px 0 0px 0; padding:2px 0 2px 5px") self.mainAreaLayout.addWidget(self.volumeSelector, 0, 1) # self.label2 = qt.QLabel("Select the slice") # self.label2.setStyleSheet("margin:0px 0 20px 7px; padding-top:20px") # self.mainAreaLayout.addWidget(self.label2, 1, 0) self.placeDefaultRulersButton = ctk.ctkPushButton() self.placeDefaultRulersButton.text = "Place default rulers" # self.placeDefaultRulersSliceButton.toolTip = "Navigate to the best estimated slice to place the rulers" self.placeDefaultRulersButton.setIcon(qt.QIcon("{0}/next.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeDefaultRulersButton.setIconSize(qt.QSize(20, 20)) self.placeDefaultRulersButton.setStyleSheet("font-weight: bold;") # self.placeDefaultRulersButton.setFixedWidth(140) self.mainAreaLayout.addWidget(self.placeDefaultRulersButton, 1, 1) ### Structure Selector self.structuresGroupbox = qt.QGroupBox("Select the structure") self.groupboxLayout = qt.QVBoxLayout() self.structuresGroupbox.setLayout(self.groupboxLayout) self.mainAreaLayout.addWidget(self.structuresGroupbox, 2, 0) self.structuresButtonGroup = qt.QButtonGroup() # btn = qt.QRadioButton("None") # btn.visible = False # self.structuresButtonGroup.addButton(btn) # self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Both") btn.checked = True self.structuresButtonGroup.addButton(btn, 0) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Pulmonary Arterial") self.structuresButtonGroup.addButton(btn, 1) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Aorta") self.structuresButtonGroup.addButton(btn, 2) self.groupboxLayout.addWidget(btn) ### Buttons toolbox self.buttonsToolboxFrame = qt.QFrame() self.buttonsToolboxLayout = qt.QGridLayout() self.buttonsToolboxFrame.setLayout(self.buttonsToolboxLayout) self.mainAreaLayout.addWidget(self.buttonsToolboxFrame, 2, 1) self.placeRulersButton = ctk.ctkPushButton() self.placeRulersButton.text = "Place ruler/s" self.placeRulersButton.toolTip = "Place the ruler/s for the selected structure/s in the current slice" self.placeRulersButton.setIcon(qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeRulersButton.setIconSize(qt.QSize(20, 20)) self.placeRulersButton.setFixedWidth(105) self.placeRulersButton.setStyleSheet("font-weight:bold") self.buttonsToolboxLayout.addWidget(self.placeRulersButton, 0, 0) self.moveUpButton = ctk.ctkPushButton() self.moveUpButton.text = "Move up" self.moveUpButton.toolTip = "Move the selected ruler/s one slice up" self.moveUpButton.setIcon(qt.QIcon("{0}/move_up.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveUpButton.setIconSize(qt.QSize(20, 20)) self.moveUpButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveUpButton, 0, 1) self.moveDownButton = ctk.ctkPushButton() self.moveDownButton.text = "Move down" self.moveDownButton.toolTip = "Move the selected ruler/s one slice down" self.moveDownButton.setIcon(qt.QIcon("{0}/move_down.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveDownButton.setIconSize(qt.QSize(20, 20)) self.moveDownButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveDownButton, 0, 2) self.removeButton = ctk.ctkPushButton() self.removeButton.text = "Remove ALL rulers" self.removeButton.toolTip = "Remove all the rulers for this volume" self.removeButton.setIcon(qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR))) self.removeButton.setIconSize(qt.QSize(20, 20)) self.buttonsToolboxLayout.addWidget(self.removeButton, 1, 1, 1, 2, 2) ### Textboxes self.textboxesFrame = qt.QFrame() self.textboxesLayout = qt.QFormLayout() self.textboxesFrame.setLayout(self.textboxesLayout) self.textboxesFrame.setFixedWidth(190) self.mainAreaLayout.addWidget(self.textboxesFrame, 3, 0) self.paTextBox = qt.QLineEdit() self.paTextBox.setReadOnly(True) self.textboxesLayout.addRow("PA (mm): ", self.paTextBox) self.aortaTextBox = qt.QLineEdit() self.aortaTextBox.setReadOnly(True) self.textboxesLayout.addRow("Aorta (mm): ", self.aortaTextBox) self.ratioTextBox = qt.QLineEdit() self.ratioTextBox.setReadOnly(True) self.textboxesLayout.addRow("Ratio PA/A: ", self.ratioTextBox) # Save case data self.reportsCollapsibleButton = ctk.ctkCollapsibleButton() self.reportsCollapsibleButton.text = "Reporting" self.layout.addWidget(self.reportsCollapsibleButton) self.reportsLayout = qt.QHBoxLayout(self.reportsCollapsibleButton) self.storedColumnNames = [ "caseId", "paDiameter_mm", "aortaDiameter_mm", "pa1r", "pa1a", "pa1s", "pa2r", "pa2a", "pa2s", "a1r", "a1a", "a1s", "a2r", "a2a", "a2s", ] self.reportsWidget = CaseReportsWidget( "CIP_PAARatio", columnNames=self.storedColumnNames, parentWidget=self.reportsCollapsibleButton ) self.reportsWidget.setup() self.switchToRedView() ##### # Case navigator 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() self.layout.addStretch() # Connections self.observers = [] self.__addSceneObservables__() self.volumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onVolumeSelectorChanged) self.placeDefaultRulersButton.connect("clicked()", self.oPlaceDefaultRulersClicked) self.placeRulersButton.connect("clicked()", self.onPlaceRulersClicked) self.moveUpButton.connect("clicked()", self.onMoveUpRulerClicked) self.moveDownButton.connect("clicked()", self.onMoveDownRulerClicked) self.removeButton.connect("clicked()", self.onRemoveRulerClicked) self.reportsWidget.addObservable(self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport)
class CIP_PAARatioWidget(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_PAARatio" 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 ): # Current module visible self.volumeSelector.setCurrentNode(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_PAARatioLogic() # # Create all the widgets. Example Area mainAreaCollapsibleButton = ctk.ctkCollapsibleButton() mainAreaCollapsibleButton.text = "Main parameters" self.layout.addWidget(mainAreaCollapsibleButton) self.mainAreaLayout = qt.QGridLayout(mainAreaCollapsibleButton) self.label = qt.QLabel("Select the volume") self.label.setStyleSheet("margin:10px 0 20px 7px") self.mainAreaLayout.addWidget(self.label, 0, 0) self.volumeSelector = slicer.qMRMLNodeComboBox() self.volumeSelector.nodeTypes = ("vtkMRMLScalarVolumeNode", "") # DEPRECATED. Now there is a new vtkMRMLLabelMapNode # self.volumeSelector.addAttribute( "vtkMRMLScalarVolumeNode", "LabelMap", "0" ) # No labelmaps self.volumeSelector.selectNodeUponCreation = True self.volumeSelector.autoFillBackground = True self.volumeSelector.addEnabled = True self.volumeSelector.noneEnabled = False self.volumeSelector.removeEnabled = False self.volumeSelector.showHidden = False self.volumeSelector.showChildNodeTypes = False self.volumeSelector.setMRMLScene(slicer.mrmlScene) self.volumeSelector.setStyleSheet("margin:0px 0 0px 0; padding:2px 0 2px 5px") self.mainAreaLayout.addWidget(self.volumeSelector, 0, 1) # self.label2 = qt.QLabel("Select the slice") # self.label2.setStyleSheet("margin:0px 0 20px 7px; padding-top:20px") # self.mainAreaLayout.addWidget(self.label2, 1, 0) self.placeDefaultRulersButton = ctk.ctkPushButton() self.placeDefaultRulersButton.text = "Place default rulers" # self.placeDefaultRulersSliceButton.toolTip = "Navigate to the best estimated slice to place the rulers" self.placeDefaultRulersButton.setIcon(qt.QIcon("{0}/next.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeDefaultRulersButton.setIconSize(qt.QSize(20, 20)) self.placeDefaultRulersButton.setStyleSheet("font-weight: bold;") # self.placeDefaultRulersButton.setFixedWidth(140) self.mainAreaLayout.addWidget(self.placeDefaultRulersButton, 1, 1) ### Structure Selector self.structuresGroupbox = qt.QGroupBox("Select the structure") self.groupboxLayout = qt.QVBoxLayout() self.structuresGroupbox.setLayout(self.groupboxLayout) self.mainAreaLayout.addWidget(self.structuresGroupbox, 2, 0) self.structuresButtonGroup = qt.QButtonGroup() # btn = qt.QRadioButton("None") # btn.visible = False # self.structuresButtonGroup.addButton(btn) # self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Both") btn.checked = True self.structuresButtonGroup.addButton(btn, 0) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Pulmonary Arterial") self.structuresButtonGroup.addButton(btn, 1) self.groupboxLayout.addWidget(btn) btn = qt.QRadioButton("Aorta") self.structuresButtonGroup.addButton(btn, 2) self.groupboxLayout.addWidget(btn) ### Buttons toolbox self.buttonsToolboxFrame = qt.QFrame() self.buttonsToolboxLayout = qt.QGridLayout() self.buttonsToolboxFrame.setLayout(self.buttonsToolboxLayout) self.mainAreaLayout.addWidget(self.buttonsToolboxFrame, 2, 1) self.placeRulersButton = ctk.ctkPushButton() self.placeRulersButton.text = "Place ruler/s" self.placeRulersButton.toolTip = "Place the ruler/s for the selected structure/s in the current slice" self.placeRulersButton.setIcon(qt.QIcon("{0}/ruler.png".format(SlicerUtil.CIP_ICON_DIR))) self.placeRulersButton.setIconSize(qt.QSize(20, 20)) self.placeRulersButton.setFixedWidth(105) self.placeRulersButton.setStyleSheet("font-weight:bold") self.buttonsToolboxLayout.addWidget(self.placeRulersButton, 0, 0) self.moveUpButton = ctk.ctkPushButton() self.moveUpButton.text = "Move up" self.moveUpButton.toolTip = "Move the selected ruler/s one slice up" self.moveUpButton.setIcon(qt.QIcon("{0}/move_up.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveUpButton.setIconSize(qt.QSize(20, 20)) self.moveUpButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveUpButton, 0, 1) self.moveDownButton = ctk.ctkPushButton() self.moveDownButton.text = "Move down" self.moveDownButton.toolTip = "Move the selected ruler/s one slice down" self.moveDownButton.setIcon(qt.QIcon("{0}/move_down.png".format(SlicerUtil.CIP_ICON_DIR))) self.moveDownButton.setIconSize(qt.QSize(20, 20)) self.moveDownButton.setFixedWidth(95) self.buttonsToolboxLayout.addWidget(self.moveDownButton, 0, 2) self.removeButton = ctk.ctkPushButton() self.removeButton.text = "Remove ALL rulers" self.removeButton.toolTip = "Remove all the rulers for this volume" self.removeButton.setIcon(qt.QIcon("{0}/delete.png".format(SlicerUtil.CIP_ICON_DIR))) self.removeButton.setIconSize(qt.QSize(20, 20)) self.buttonsToolboxLayout.addWidget(self.removeButton, 1, 1, 1, 2, 2) ### Textboxes self.textboxesFrame = qt.QFrame() self.textboxesLayout = qt.QFormLayout() self.textboxesFrame.setLayout(self.textboxesLayout) self.textboxesFrame.setFixedWidth(190) self.mainAreaLayout.addWidget(self.textboxesFrame, 3, 0) self.paTextBox = qt.QLineEdit() self.paTextBox.setReadOnly(True) self.textboxesLayout.addRow("PA (mm): ", self.paTextBox) self.aortaTextBox = qt.QLineEdit() self.aortaTextBox.setReadOnly(True) self.textboxesLayout.addRow("Aorta (mm): ", self.aortaTextBox) self.ratioTextBox = qt.QLineEdit() self.ratioTextBox.setReadOnly(True) self.textboxesLayout.addRow("Ratio PA/A: ", self.ratioTextBox) # Save case data self.reportsCollapsibleButton = ctk.ctkCollapsibleButton() self.reportsCollapsibleButton.text = "Reporting" self.layout.addWidget(self.reportsCollapsibleButton) self.reportsLayout = qt.QHBoxLayout(self.reportsCollapsibleButton) self.storedColumnNames = [ "caseId", "paDiameter_mm", "aortaDiameter_mm", "pa1r", "pa1a", "pa1s", "pa2r", "pa2a", "pa2s", "a1r", "a1a", "a1s", "a2r", "a2a", "a2s", ] self.reportsWidget = CaseReportsWidget( "CIP_PAARatio", columnNames=self.storedColumnNames, parentWidget=self.reportsCollapsibleButton ) self.reportsWidget.setup() self.switchToRedView() ##### # Case navigator 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() self.layout.addStretch() # Connections self.observers = [] self.__addSceneObservables__() self.volumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onVolumeSelectorChanged) self.placeDefaultRulersButton.connect("clicked()", self.oPlaceDefaultRulersClicked) self.placeRulersButton.connect("clicked()", self.onPlaceRulersClicked) self.moveUpButton.connect("clicked()", self.onMoveUpRulerClicked) self.moveDownButton.connect("clicked()", self.onMoveDownRulerClicked) self.removeButton.connect("clicked()", self.onRemoveRulerClicked) self.reportsWidget.addObservable(self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) def enter(self): """This is invoked every time that we select this module as the active module in Slicer (not only the first time)""" # activeVolumeId = SlicerUtil.getActiveVolumeIdInRedSlice() # if activeVolumeId is not None: # self.volumeSelector.setCurrentNodeID(activeVolumeId) # if activeVolumeId not in self.logic.currentVolumesLoaded: # self.placeDefaultRulers(activeVolumeId) volumeId = self.volumeSelector.currentNodeId if volumeId: SlicerUtil.setActiveVolumeId(volumeId) def exit(self): """This is invoked every time that we switch to another module (not only when Slicer is closed).""" pass def cleanup(self): """This is invoked as a destructor of the GUI when the module is no longer going to be used""" pass def jumpToTemptativeSlice(self, volumeId): """ Jump the red window to a predefined slice based on the size of the volume :param volumeId: """ # Get the default coordinates of the ruler aorta1, aorta2, pa1, pa2 = self.logic.getDefaultCoords(volumeId) # Set the display in the right slice self.moveRedWindowToSlice(aorta1[2]) def placeDefaultRulers(self, volumeId): """ Set the Aorta and PA rulers to a default estimated position and jump to that slice :param volumeId: """ if not volumeId: return # Hide all the actual ruler nodes self.logic.hideAllRulers() # Remove the current rulers for this volume self.logic.removeRulers(volumeId) # Create the default rulers self.logic.createDefaultRulers(volumeId, self.onRulerUpdated) # Activate both structures self.structuresButtonGroup.buttons()[0].setChecked(True) # Jump to the slice where the rulers are self.jumpToTemptativeSlice(volumeId) # Place the rulers in the current slice self.placeRuler() # Add the current volume to the list of loaded volumes self.logic.currentVolumesLoaded.add(volumeId) # Modify the zoom of the Red slice redSliceNode = slicer.util.getFirstNodeByClassByName("vtkMRMLSliceNode", "Red") factor = 0.5 newFOVx = redSliceNode.GetFieldOfView()[0] * factor newFOVy = redSliceNode.GetFieldOfView()[1] * factor newFOVz = redSliceNode.GetFieldOfView()[2] redSliceNode.SetFieldOfView(newFOVx, newFOVy, newFOVz) # Move the camera up to fix the view redSliceNode.SetXYZOrigin(0, 50, 0) # Refresh the data in the viewer redSliceNode.UpdateMatrices() def placeRuler(self): """ Place one or the two rulers in the current visible slice in Red node """ volumeId = self.volumeSelector.currentNodeId if volumeId == "": self.showUnselectedVolumeWarningMessage() return selectedStructure = self.getCurrentSelectedStructure() if selectedStructure == self.logic.NONE: qt.QMessageBox.warning( slicer.util.mainWindow(), "Review structure", "Please select Pulmonary Arterial, Aorta or both to place the right ruler/s", ) return # Get the current slice currentSlice = self.getCurrentRedWindowSlice() if selectedStructure == self.logic.BOTH: structures = [self.logic.PA, self.logic.AORTA] else: structures = [selectedStructure] for structure in structures: self.logic.placeRulerInSlice(volumeId, structure, currentSlice, self.onRulerUpdated) self.refreshTextboxes() def getCurrentSelectedStructure(self): """ Get the current selected structure id :return: self.logic.AORTA or self.logic.PA """ selectedStructureText = self.structuresButtonGroup.checkedButton().text if selectedStructureText == "Aorta": return self.logic.AORTA elif selectedStructureText == "Pulmonary Arterial": return self.logic.PA elif selectedStructureText == "Both": return self.logic.BOTH return self.logic.NONE def stepSlice(self, offset): """ Move the selected structure one slice up or down :param offset: +1 or -1 :return: """ volumeId = self.volumeSelector.currentNodeId if volumeId == "": self.showUnselectedVolumeWarningMessage() return selectedStructure = self.getCurrentSelectedStructure() if selectedStructure == self.logic.NONE: self.showUnselectedStructureWarningMessage() return if selectedStructure == self.logic.BOTH: # Move both rulers self.logic.stepSlice(volumeId, self.logic.AORTA, offset) newSlice = self.logic.stepSlice(volumeId, self.logic.PA, offset) else: newSlice = self.logic.stepSlice(volumeId, selectedStructure, offset) self.moveRedWindowToSlice(newSlice) def removeRulers(self): """ Remove all the rulers related to the current volume node :return: """ self.logic.removeRulers(self.volumeSelector.currentNodeId) self.refreshTextboxes(reset=True) def getCurrentRedWindowSlice(self): """ Get the current slice (in RAS) of the Red window :return: """ redNodeSliceNode = slicer.app.layoutManager().sliceWidget("Red").sliceLogic().GetSliceNode() return redNodeSliceNode.GetSliceOffset() def moveRedWindowToSlice(self, newSlice): """ Moves the red display to the specified RAS slice :param newSlice: slice to jump (RAS format) :return: """ redNodeSliceNode = slicer.app.layoutManager().sliceWidget("Red").sliceLogic().GetSliceNode() redNodeSliceNode.JumpSlice(0, 0, newSlice) def refreshTextboxes(self, reset=False): """ Update the information of the textboxes that give information about the measurements """ self.aortaTextBox.setText("0") self.paTextBox.setText("0") self.ratioTextBox.setText("0") self.ratioTextBox.setStyleSheet(" QLineEdit { background-color: white; color: black}") volumeId = self.volumeSelector.currentNodeId if volumeId not in self.logic.currentVolumesLoaded: return if volumeId: self.logic.changeColor(volumeId, self.logic.defaultColor) aorta = None pa = None if not reset: rulerAorta, newAorta = self.logic.getRulerNodeForVolumeAndStructure( self.volumeSelector.currentNodeId, self.logic.AORTA, createIfNotExist=False ) rulerPA, newPA = self.logic.getRulerNodeForVolumeAndStructure( self.volumeSelector.currentNodeId, self.logic.PA, createIfNotExist=False ) if rulerAorta: aorta = rulerAorta.GetDistanceMeasurement() self.aortaTextBox.setText(str(aorta)) if rulerPA: pa = rulerPA.GetDistanceMeasurement() self.paTextBox.setText(str(pa)) if aorta is not None and aorta != 0: try: ratio = pa / aorta self.ratioTextBox.setText(str(ratio)) if ratio > 1: # Switch colors ("alarm") self.ratioTextBox.setStyleSheet(" QLineEdit { background-color: rgb(255, 0, 0); color: white}") self.logic.changeColor(volumeId, self.logic.defaultWarningColor) except Exception: Util.print_last_exception() def showUnselectedVolumeWarningMessage(self): qt.QMessageBox.warning(slicer.util.mainWindow(), "Select a volume", "Please select a volume") def showUnselectedStructureWarningMessage(self): qt.QMessageBox.warning( slicer.util.mainWindow(), "Review structure", "Please select Aorta, Pulmonary Arterial or Both to place the right ruler/s", ) def switchToRedView(self): """ Switch the layout to Red slice only :return: """ layoutManager = slicer.app.layoutManager() layoutManager.setLayout(6) def __addSceneObservables__(self): self.observers.append( slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.NodeAddedEvent, self.__onNodeAddedObserver__) ) self.observers.append(slicer.mrmlScene.AddObserver(slicer.vtkMRMLScene.EndCloseEvent, self.__onSceneClosed__)) def __removeSceneObservables(self): for observer in self.observers: slicer.mrmlScene.RemoveObserver(observer) self.observers.remove(observer) ######### # EVENTS def onVolumeSelectorChanged(self, node): # if node is not None and node.GetID() not in self.currentVolumesLoaded: # if node is not None: # # New node. Load default rulers # if node.GetID() not in self.logic.currentVolumesLoaded: # self.placeDefaultRulers(node.GetID()) self.refreshTextboxes() def onStructureClicked(self, button): fiducialsNode = self.getFiducialsNode(self.volumeSelector.currentNodeId) if fiducialsNode is not None: self.__addRuler__(button.text, self.volumeSelector.currentNodeId) markupsLogic = slicer.modules.markups.logic() markupsLogic.SetActiveListID(fiducialsNode) applicationLogic = slicer.app.applicationLogic() selectionNode = applicationLogic.GetSelectionNode() selectionNode.SetReferenceActivePlaceNodeClassName("vtkMRMLAnnotationRulerNode") interactionNode = applicationLogic.GetInteractionNode() interactionNode.SwitchToSinglePlaceMode() def oPlaceDefaultRulersClicked(self): volumeId = self.volumeSelector.currentNodeId if volumeId == "": self.showUnselectedVolumeWarningMessage() return self.placeDefaultRulers(volumeId) def onRulerUpdated(self, node, event): self.refreshTextboxes() def onPlaceRulersClicked(self): self.placeRuler() def onMoveUpRulerClicked(self): self.stepSlice(1) def onMoveDownRulerClicked(self): self.stepSlice(-1) def onRemoveRulerClicked(self): if ( qt.QMessageBox.question( slicer.util.mainWindow(), "Remove rulers", "Are you sure you want to remove all the rulers from this volume?", qt.QMessageBox.Yes | qt.QMessageBox.No, ) ) == qt.QMessageBox.Yes: self.logic.removeRulers(self.volumeSelector.currentNodeId) self.refreshTextboxes() def onSaveReport(self): """ Save the current values in a persistent csv file :return: """ volumeId = self.volumeSelector.currentNodeId if volumeId: caseName = slicer.mrmlScene.GetNodeByID(volumeId).GetName() coords = [0, 0, 0, 0] pa1 = pa2 = a1 = a2 = None # PA rulerNode, newNode = self.logic.getRulerNodeForVolumeAndStructure( volumeId, self.logic.PA, createIfNotExist=False ) if rulerNode: # Get current RAS coords rulerNode.GetPositionWorldCoordinates1(coords) pa1 = list(coords) rulerNode.GetPositionWorldCoordinates2(coords) pa2 = list(coords) # AORTA rulerNode, newNode = self.logic.getRulerNodeForVolumeAndStructure( volumeId, self.logic.AORTA, createIfNotExist=False ) if rulerNode: rulerNode.GetPositionWorldCoordinates1(coords) a1 = list(coords) rulerNode.GetPositionWorldCoordinates2(coords) a2 = list(coords) self.reportsWidget.saveCurrentValues( caseId=caseName, paDiameter_mm=self.paTextBox.text, aortaDiameter_mm=self.aortaTextBox.text, pa1r=pa1[0] if pa1 is not None else "", pa1a=pa1[1] if pa1 is not None else "", pa1s=pa1[2] if pa1 is not None else "", pa2r=pa2[0] if pa2 is not None else "", pa2a=pa2[1] if pa2 is not None else "", pa2s=pa2[2] if pa2 is not None else "", a1r=a1[0] if a1 is not None else "", a1a=a1[1] if a1 is not None else "", a1s=a1[2] if a1 is not None else "", a2r=a2[0] if a2 is not None else "", a2a=a2[1] if a2 is not None else "", a2s=a2[2] if a2 is not None else "", ) qt.QMessageBox.information(slicer.util.mainWindow(), "Data saved", "The data were saved successfully") def __onSceneClosed__(self, arg1, arg2): """ Scene closed. Reset currently loaded volumes :param arg1: :param arg2: :return: """ self.logic.currentVolumesLoaded.clear()
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 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 volume #self.caseNavigatorWidget.addObservable(self.caseNavigatorWidget.EVENT_VOLUME_LOADED, self.onNewVolumeLoaded) 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 (str)", 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__))
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) 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 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 volume #self.caseNavigatorWidget.addObservable(self.caseNavigatorWidget.EVENT_VOLUME_LOADED, self.onNewVolumeLoaded) 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 (str)", 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): 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) self.logic.saveCurrentFiducials(d) qt.QMessageBox.information(slicer.util.mainWindow(), 'Results saved', "The results have been saved succesfully") except: qt.QMessageBox.warning(slicer.util.mainWindow(), 'Directory incorrect', 'The folder "{0}" could not be created. Please select a valid directory'.format(d)) else: self.logic.saveCurrentFiducials(d) qt.QMessageBox.information(slicer.util.mainWindow(), 'Results saved', "The results have been saved succesfully") 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.loadFiducials(volumeNode, f) self.saveResultsDirectoryButton.directory = os.path.dirname(f) qt.QMessageBox.information(slicer.util.mainWindow(), "File loaded", "File loaded successfully") 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.util.getNode(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: """ self.__checkNewVolume__(newVolumeNode) self.blockNodeEvents = True self.volumeSelector.setCurrentNode(newVolumeNode) self.blockNodeEvents = False def __onCurrentNodeChanged__(self, volumeNode): self.__checkNewVolume__(volumeNode) # if volumeNode: # #self.logic.reset(volumeNode) # SlicerUtil.setActiveVolumeId(volumeNode.GetID()) # self.updateState() def __checkNewVolume__(self, 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.GetID()): # 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.setActiveVolumeId(newVolumeNode.GetID()) SlicerUtil.setFiducialsCursorMode(True, True) self.currentVolumeLoaded = newVolumeNode self.updateState() self.blockNodeEvents = False 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