class CIP_ParenchymaAnalysisWidget(ScriptedLoadableModuleWidget): @property def moduleName(self): return os.path.basename(__file__).replace(".py", "") def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) self.chartOptions = ("LAA%-950", "LAA%-925", "LAA%-910", "LAA%-856", "HAA%-700", "HAA%-600", "HAA%-500", "HAA%-250", "Perc10", "Perc15", "Mean", "Std", "Kurtosis", "Skewness", "Ventilation Heterogeneity", "Mass", "Volume") # Build the column keys. Here all the columns are declared, but an alternative could be just: #self.columnsDict = CaseReportsWidget.getColumnKeysNormalizedDictionary(["Volume Name", "Region", "LAA%-950", ...]) self.columnsDict = OrderedDict() self.columnsDict["VolumeName"] = "Volume Name" self.columnsDict["Region"] = "Region" self.columnsDict["LAA950"] = "LAA%-950" self.columnsDict["LAA925"] = "LAA%-925" self.columnsDict["LAA910"] = "LAA%-910" self.columnsDict["LAA856"] = "LAA%-856" self.columnsDict["HAA700"] = "HAA%-700" self.columnsDict["HAA600"] = "HAA%-600" self.columnsDict["HAA500"] = "HAA%-500" self.columnsDict["HAA250"] = "HAA%-250" self.columnsDict["Perc10"] = "Perc10" self.columnsDict["Perc15"] = "Perc15" self.columnsDict["Mean"] = "Mean" self.columnsDict["Std"] = "Std" self.columnsDict["Kurtosis"] = "Kurtosis" self.columnsDict["Skewness"] = "Skewness" self.columnsDict[ "VentilationHeterogeneity"] = "Ventilation Heterogeneity" self.columnsDict["Mass"] = "Mass" self.columnsDict["Volume"] = "Volume" self.rTags = ("WholeLung", "RightLung", "LeftLung", "RUL", "RLL", "RML", "LUL", "LLL", "LUT", "LMT", "LLT", "RUT", "RMT", "RLT") if not parent: self.parent = slicer.qMRMLWidget() self.parent.setLayout(qt.QVBoxLayout()) self.parent.setMRMLScene(slicer.mrmlScene) else: self.parent = parent self.logic = None self.CTNode = None self.labelNode = None # self.expNode = None # self.explabelNode = None self.fileName = None self.fileDialog = None if not parent: self.setup() self.CTSelector.setMRMLScene(slicer.mrmlScene) # self.expSelector.setMRMLScene(slicer.mrmlScene) self.labelSelector.setMRMLScene(slicer.mrmlScene) # self.explabelSelector.setMRMLScene(slicer.mrmlScene) self.parent.show() def enter(self): if self.labelSelector.currentNode(): for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetLabelVolumeID( self.labelSelector.currentNode().GetID()) def exit(self): for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetLabelVolumeID('None') def setup(self): ScriptedLoadableModuleWidget.setup(self) # # the inps volume selector # parametersCollapsibleButton = ctk.ctkCollapsibleButton() parametersCollapsibleButton.text = "IO Volumes" self.parent.layout().addWidget(parametersCollapsibleButton) # Layout within the dummy collapsible button parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) parametersFormLayout.setVerticalSpacing(5) self.CTSelector = slicer.qMRMLNodeComboBox() self.CTSelector.nodeTypes = (("vtkMRMLScalarVolumeNode"), "") self.CTSelector.addAttribute("vtkMRMLScalarVolumeNode", "LabelMap", 0) self.CTSelector.selectNodeUponCreation = False self.CTSelector.addEnabled = False self.CTSelector.removeEnabled = False self.CTSelector.noneEnabled = True self.CTSelector.showHidden = False self.CTSelector.showChildNodeTypes = False self.CTSelector.setMRMLScene(slicer.mrmlScene) self.CTSelector.setToolTip("Pick the CT image to work on.") parametersFormLayout.addRow("Input CT Volume: ", self.CTSelector) # # the label map volume selector # self.labelSelector = slicer.qMRMLNodeComboBox() # self.labelSelector.nodeTypes = ( ("vtkMRMLScalarVolumeNode"), "" ) # self.labelSelector.addAttribute( "vtkMRMLScalarVolumeNode", "LabelMap", 1 ) self.labelSelector.nodeTypes = (("vtkMRMLLabelMapVolumeNode"), "") self.labelSelector.selectNodeUponCreation = True self.labelSelector.addEnabled = False self.labelSelector.removeEnabled = False self.labelSelector.noneEnabled = True self.labelSelector.showHidden = False self.labelSelector.showChildNodeTypes = False self.labelSelector.setMRMLScene(slicer.mrmlScene) self.labelSelector.setToolTip("Pick the label map to the algorithm.") parametersFormLayout.addRow("Label Map Volume: ", self.labelSelector) # Image filtering section self.preProcessingWidget = PreProcessingWidget( self.moduleName, parentWidget=self.parent) self.preProcessingWidget.setup() # # self.splitRadioButton = qt.QRadioButton() # self.splitRadioButton.setText('Split Label Map') # self.splitRadioButton.setChecked(0) # self.parent.layout().addWidget(self.splitRadioButton, 0, 3) # Apply button self.applyButton = qt.QPushButton("Apply") self.applyButton.toolTip = "Calculate Parenchyma Phenotypes." self.applyButton.enabled = False self.applyButton.setFixedSize(300, 30) self.parent.layout().addWidget(self.applyButton, 0, 4) # model and view for stats table self.view = qt.QTableView() self.view.sortingEnabled = True self.parent.layout().addWidget(self.view) # model and view for EXP stats table """self.viewexp = qt.QTableView() self.viewexp.sortingEnabled = True self.parent.layout().addWidget(self.viewexp)""" # Histogram Selection self.HistSection = qt.QFrame() self.HistSection.setLayout(qt.QVBoxLayout()) self.parent.layout().addWidget(self.HistSection) self.HistSection.setObjectName('HistSection') self.HistSection.setStyleSheet( '#HistSection {border: 0.5px solid lightGray; }') HistSectionTitle = qt.QLabel() HistSectionTitle.setText('Histogram Section') # HistSectionTitle.setStyleSheet('border: 1px solid white; color: black') self.HistSection.layout().addWidget(HistSectionTitle) self.histogramCheckBoxes = [] self.histFrame = qt.QFrame() # self.histFrame.setStyleSheet('border: 1px solid white') self.histFrame.setLayout(qt.QHBoxLayout()) self.GlobalHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.GlobalHistCheckBox) self.histFrame.layout().addWidget(self.GlobalHistCheckBox) self.RightHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RightHistCheckBox) self.histFrame.layout().addWidget(self.RightHistCheckBox) self.LeftHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LeftHistCheckBox) self.histFrame.layout().addWidget(self.LeftHistCheckBox) self.RULHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RULHistCheckBox) self.histFrame.layout().addWidget(self.RULHistCheckBox) self.RLLHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RLLHistCheckBox) self.histFrame.layout().addWidget(self.RLLHistCheckBox) self.RMLHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RMLHistCheckBox) self.histFrame.layout().addWidget(self.RMLHistCheckBox) self.LULHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LULHistCheckBox) self.histFrame.layout().addWidget(self.LULHistCheckBox) self.LLLHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LLLHistCheckBox) self.histFrame.layout().addWidget(self.LLLHistCheckBox) self.LUTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LUTHistCheckBox) self.histFrame.layout().addWidget(self.LUTHistCheckBox) self.LMTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LMTHistCheckBox) self.histFrame.layout().addWidget(self.LMTHistCheckBox) self.LLTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.LLTHistCheckBox) self.histFrame.layout().addWidget(self.LLTHistCheckBox) self.RUTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RUTHistCheckBox) self.histFrame.layout().addWidget(self.RUTHistCheckBox) self.RMTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RMTHistCheckBox) self.histFrame.layout().addWidget(self.RMTHistCheckBox) self.RLTHistCheckBox = qt.QCheckBox() self.histogramCheckBoxes.append(self.RLTHistCheckBox) self.histFrame.layout().addWidget(self.RLTHistCheckBox) for i in xrange(len(self.histogramCheckBoxes)): self.histogramCheckBoxes[i].setText(self.rTags[i]) self.histogramCheckBoxes[i].hide() self.HistSection.layout().addWidget(self.histFrame) self.HistSection.enabled = False # Chart button self.chartBox = qt.QFrame() self.chartBox.setObjectName("chartBox") self.chartBox.setStyleSheet( '#chartBox {border: 0.5px solid lightGray;}') self.chartBox.setLayout(qt.QVBoxLayout()) self.parent.layout().addWidget(self.chartBox) chartSectionTitle = qt.QLabel() chartSectionTitle.setText('Chart Section') self.chartBox.layout().addWidget(chartSectionTitle) chartFrame = qt.QFrame() chartFrame.setLayout(qt.QHBoxLayout()) self.chartBox.layout().addWidget(chartFrame) self.chartButton = qt.QPushButton("Chart") self.chartButton.toolTip = "Make a chart from the current statistics." chartFrame.layout().addWidget(self.chartButton) self.chartOption = qt.QComboBox() self.chartOption.addItems(self.chartOptions) chartFrame.layout().addWidget(self.chartOption) self.chartBox.enabled = False self.reportsWidget = CaseReportsWidget(self.moduleName, self.columnsDict, parentWidget=self.parent) self.reportsWidget.setup() self.reportsWidget.showPrintButton(True) # self.reportsWidget.saveButton.enabled = False # self.reportsWidget.openButton.enabled = False # self.reportsWidget.exportButton.enabled = False # self.reportsWidget.removeButton.enabled = False # By default, the Print button is hidden # self.reportsWidget.showPrintButton.enabled = False # Add vertical spacer self.parent.layout().addStretch(1) # connections self.applyButton.connect('clicked()', self.onApply) self.chartButton.connect('clicked()', self.onChart) self.reportsWidget.addObservable( self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) self.reportsWidget.addObservable( self.reportsWidget.EVENT_PRINT_BUTTON_CLICKED, self.onPrintReport) self.CTSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onCTSelect) self.labelSelector.connect('currentNodeChanged(vtkMRMLNode*)', self.onLabelSelect) self.GlobalHistCheckBox.connect('clicked()', self.onHistogram) self.RightHistCheckBox.connect('clicked()', self.onHistogram) self.LeftHistCheckBox.connect('clicked()', self.onHistogram) self.RULHistCheckBox.connect('clicked()', self.onHistogram) self.RLLHistCheckBox.connect('clicked()', self.onHistogram) self.RMLHistCheckBox.connect('clicked()', self.onHistogram) self.LULHistCheckBox.connect('clicked()', self.onHistogram) self.LLLHistCheckBox.connect('clicked()', self.onHistogram) self.LUTHistCheckBox.connect('clicked()', self.onHistogram) self.LMTHistCheckBox.connect('clicked()', self.onHistogram) self.LLTHistCheckBox.connect('clicked()', self.onHistogram) self.RUTHistCheckBox.connect('clicked()', self.onHistogram) self.RMTHistCheckBox.connect('clicked()', self.onHistogram) self.RLTHistCheckBox.connect('clicked()', self.onHistogram) def cleanup(self): self.reportsWidget.cleanup() self.reportsWidget = None def onCTSelect(self, node): self.CTNode = node self.applyButton.enabled = bool( self.CTNode) # and bool(self.labelNode) self.preProcessingWidget.enableFilteringFrame(bool(self.CTNode)) self.preProcessingWidget.enableLMFrame(bool(not self.labelNode)) if self.CTNode: for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetBackgroundVolumeID( self.CTNode.GetID()) else: for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetBackgroundVolumeID('None') def onLabelSelect(self, node): self.labelNode = node self.applyButton.enabled = bool( self.CTNode) # and bool(self.labelNode) self.preProcessingWidget.enableFilteringFrame(bool(self.CTNode)) self.preProcessingWidget.enableLMFrame(bool(not self.labelNode)) SlicerUtil.changeLabelmapOpacity(0.5) if self.labelNode: self.preProcessingWidget.filterApplication.setChecked(1) self.preProcessingWidget.filterApplication.setEnabled(0) for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetLabelVolumeID( self.labelNode.GetID()) else: self.preProcessingWidget.filterApplication.setChecked(0) self.preProcessingWidget.filterApplication.setEnabled(1) for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetLabelVolumeID('None') def inputVolumesAreValid(self): """Verify that volumes are compatible with label calculation algorithm assumptions""" if not self.CTNode: # or not self.labelNode: qt.QMessageBox.warning(slicer.util.mainWindow(), "Parenchyma Analysis", "Please select a CT Input Volume.") return False if not self.CTNode.GetImageData( ): # or not self.labelNode.GetImageData(): qt.QMessageBox.warning(slicer.util.mainWindow(), "Parenchyma Analysis", "Please select a CT Input Volume.") return False if not self.labelNode or not self.labelNode.GetImageData(): warning = self.preProcessingWidget.warningMessageForLM() if warning == 16384: self.createLungLabelMap() else: qt.QMessageBox.warning(slicer.util.mainWindow(), "Parenchyma Analysis", "Please select a Lung Label Map.") return False return True if self.CTNode.GetImageData().GetDimensions( ) != self.labelNode.GetImageData().GetDimensions(): qt.QMessageBox.warning( slicer.util.mainWindow(), "Parenchyma Analysis", "Input Volumes do not have the same geometry.") return False # if self.preProcessingWidget.filterOnRadioButton.checked: # self.preProcessingWidget.filterApplication.setChecked(1) return True def filterInputCT(self): self.applyButton.enabled = False self.applyButton.text = "Filtering..." # TODO: why doesn't processEvents alone make the label text change? self.applyButton.repaint() slicer.app.processEvents() self.preProcessingWidget.filterInputCT(self.CTNode) def createLungLabelMap(self): """Create the lung label map """ self.applyButton.enabled = False if self.preProcessingWidget.filterOnRadioButton.checked: # and not self.preProcessingWidget.filterApplication.checked: self.filterInputCT() inputNode = self.CTNode self.applyButton.text = "Creating Label Map..." # TODO: why doesn't processEvents alone make the label text change? self.applyButton.repaint() slicer.app.processEvents() self.labelNode = slicer.mrmlScene.AddNode( slicer.vtkMRMLLabelMapVolumeNode()) name = inputNode.GetName() + '_partialLungLabelMap' self.labelNode.SetName(slicer.mrmlScene.GenerateUniqueName(name)) self.preProcessingWidget.createPartialLM(inputNode, self.labelNode) label_image = self.labelNode.GetImageData() shape = list(label_image.GetDimensions()) input_array = vtk.util.numpy_support.vtk_to_numpy( label_image.GetPointData().GetScalars()) original_shape = input_array.shape input_array = input_array.reshape( shape[2], shape[1], shape[0]) # input_array.transpose([2, 1, 0]) would not work! input_image = sitk.GetImageFromArray(input_array) input_image.SetSpacing(self.labelNode.GetSpacing()) input_image.SetOrigin(self.labelNode.GetOrigin()) my_lung_splitter = lung_splitter(split_thirds=True) split_lm = my_lung_splitter.execute(input_image) split = sitk.GetArrayFromImage(split_lm) input_aa = vtk.util.numpy_support.vtk_to_numpy( label_image.GetPointData().GetScalars()) input_aa[:] = split.reshape(original_shape) self.labelNode.StorableModified() self.labelNode.Modified() self.labelNode.InvokeEvent( slicer.vtkMRMLVolumeNode.ImageDataModifiedEvent, self.labelNode) SlicerUtil.changeLabelmapOpacity(0.5) def onApply(self): """Calculate the parenchyma analysis """ if not self.inputVolumesAreValid(): return self.applyButton.enabled = False if self.preProcessingWidget.filterOnRadioButton.checked and self.preProcessingWidget.filterApplication.checked: self.filterInputCT() self.applyButton.text = "Analysing..." # TODO: why doesn't processEvents alone make the label text change? self.applyButton.repaint() slicer.app.processEvents() self.logic = CIP_ParenchymaAnalysisLogic(self.CTNode, self.labelNode) self.populateStats() self.logic.createHistogram() for i in xrange(len(self.histogramCheckBoxes)): self.histogramCheckBoxes[i].setChecked(0) self.histogramCheckBoxes[i].hide() for tag in self.rTags: if tag in self.logic.regionTags: self.histogramCheckBoxes[self.rTags.index(tag)].show() self.HistSection.enabled = True self.chartBox.enabled = True # self.reportsWidget.saveButton.enabled = True # self.reportsWidget.openButton.enabled = True # self.reportsWidget.exportButton.enabled = True # self.reportsWidget.removeButton.enabled = True self.applyButton.enabled = True self.applyButton.text = "Apply" for color in ['Red', 'Yellow', 'Green']: slicer.app.layoutManager().sliceWidget(color).sliceLogic( ).GetSliceCompositeNode().SetBackgroundVolumeID( self.CTNode.GetID()) self.labelSelector.setCurrentNode(self.labelNode) def onHistogram(self): """Histogram of the selected region """ self.histList = [] for i in xrange(len(self.histogramCheckBoxes)): if self.histogramCheckBoxes[i].checked == True: self.histList.append(self.rTags[i]) self.logic.AddSelectedHistograms(self.histList) def onChart(self): """chart the parenchyma analysis """ valueToPlot = self.chartOptions[self.chartOption.currentIndex] self.logic.createStatsChart(self.labelNode, valueToPlot) def onSaveReport(self): """ Save the current values in a persistent csv file """ self.logic.statsAsCSV(self.reportsWidget, self.CTNode) def onPrintReport(self): """ Print a pdf report """ emphysema_image_path, ct_slice_path = self.logic.computeEmphysemaOnSlice( self.CTNode, self.labelNode, op=0.5) pdfReporter = PdfReporter() # Get the values that are going to be inserted in the html template caseName = self.CTNode.GetName() values = dict() values["@@PATH_TO_STATIC@@"] = os.path.join( os.path.dirname(os.path.realpath(__file__)), "Resources/") values["@@SUBJECT@@"] = "Subject: " + str(caseName) values["@@GLOBAL_LEVEL@@"] = "{:.2f}%".format( self.logic.labelStats['LAA%-950', 'WholeLung']) values["@@SUMMARY@@"] = "Emphysema per region: " pdfRows = """""" for tag in self.logic.regionTags: pdfRows += """<tr> <td align="center">{} </td> <td align="center">{:.2f} </td> </tr>""".format(tag, self.logic.labelStats['LAA%-950', tag]) values["@@TABLE_ROWS@@"] = pdfRows # Get the path to the html template htmlTemplatePath = os.path.join( os.path.dirname(os.path.realpath(__file__)), "Resources/CIP_ParenchymaAnalysisReport.html") # Get a list of image absolute paths that may be needed for the report. In this case, we get the ACIL logo imagesFileList = [SlicerUtil.ACIL_LOGO_PATH] values["@@EMPHYSEMA_IMAGE@@"] = emphysema_image_path values["@@CT_IMAGE@@"] = ct_slice_path # Print the report. Remember that we can optionally specify the absolute path where the report is going to # be stored pdfReporter.printPdf(htmlTemplatePath, values, self.reportPrinted, imagesFileList=imagesFileList) def reportPrinted(self, reportPath): Util.openFile(reportPath) def onFileSelected(self, fileName): self.logic.saveStats(fileName) def populateStats(self): if not self.logic: return displayNode = self.labelNode.GetDisplayNode() colorNode = displayNode.GetColorNode() lut = colorNode.GetLookupTable() self.items = [] self.model = qt.QStandardItemModel() self.view.setModel(self.model) self.view.verticalHeader().visible = False row = 0 for regionTag, regionValue in zip(self.logic.regionTags, self.logic.regionValues): color = qt.QColor() rgb = lut.GetTableValue(regionValue[0]) color.setRgb(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255) item = qt.QStandardItem() item.setData(color, 1) item.setText(str(regionTag)) item.setData(regionTag, 1) item.setToolTip(regionTag) item.setTextAlignment(1) self.model.setItem(row, 0, item) self.items.append(item) col = 1 for k in self.logic.keys: item = qt.QStandardItem() item.setText("%.3f" % self.logic.labelStats[k, regionTag]) item.setTextAlignment(4) self.view.setColumnWidth(col, 15 * len(item.text())) self.model.setItem(row, col, item) self.items.append(item) col += 1 row += 1 self.view.setColumnWidth(0, 15 * len('Region')) self.model.setHeaderData(0, 1, "Region") col = 1 for k in self.logic.keys: # self.view.setColumnWidth(col,15*len(k)) self.model.setHeaderData(col, 1, k) col += 1
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
class CIP_CalciumScoringWidget(ScriptedLoadableModuleWidget): def __init__(self, parent=None): print "init" ScriptedLoadableModuleWidget.__init__(self, parent) # Default variables # ----------------- self.calcificationType = 0 self.ThresholdMin = 130.0 self.ThresholdMax = 1000.0 self.MinimumLesionSize = 1 self.MaximumLesionSize = 500 self.selectedLabelList = [] self.selectedLabels = {} self.modelNodes = [] self.selectedRGB = [1, 0, 0] self.summary_reports = [ "Agatston Score 2D", "Agatston Score 3D", "Mass Score", "Volume" ] self.labelScores = dict() self.totalScores = dict() for sr in self.summary_reports: self.labelScores[sr] = [] self.totalScores[sr] = 0 self.columnsDict = OrderedDict() self.columnsDict["CaseID"] = "CaseID" for sr in self.summary_reports: self.columnsDict[sr.replace(" ", "")] = sr def setup(self): print "setup()" self.setInteractor() # Instantiate and connect widgets # ------------------------------- ScriptedLoadableModuleWidget.setup(self) # Parameters Area # --------------- parametersCollapsibleButton = ctk.ctkCollapsibleButton() parametersCollapsibleButton.text = "Parameters" self.layout.addWidget(parametersCollapsibleButton) parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) # Target volume selector # ---------------------- self.inputSelector = slicer.qMRMLNodeComboBox() self.inputSelector.nodeTypes = (("vtkMRMLScalarVolumeNode"), "") self.inputSelector.addEnabled = False self.inputSelector.removeEnabled = False self.inputSelector.noneEnabled = False self.inputSelector.showHidden = False self.inputSelector.showChildNodeTypes = False self.inputSelector.setMRMLScene(slicer.mrmlScene) self.inputSelector.setToolTip("Pick the input to the algorithm.") parametersFormLayout.addRow("Target Volume: ", self.inputSelector) self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onVolumeChanged) self.volumeNode = self.inputSelector.currentNode() # Thresholds selectors # -------------------- self.ThresholdRange = ctk.ctkRangeWidget() self.ThresholdRange.minimum = 0 self.ThresholdRange.maximum = 2000 self.ThresholdRange.setMinimumValue(self.ThresholdMin) self.ThresholdRange.setMaximumValue(self.ThresholdMax) self.ThresholdRange.connect("minimumValueChanged(double)", self.onThresholdMinChanged) self.ThresholdRange.connect("maximumValueChanged(double)", self.onThresholdMaxChanged) parametersFormLayout.addRow("Threshold Value", self.ThresholdRange) self.ThresholdRange.setMinimumValue(self.ThresholdMin) self.ThresholdRange.setMaximumValue(self.ThresholdMax) self.LesionSizeRange = ctk.ctkRangeWidget() self.LesionSizeRange.minimum = 0.5 self.LesionSizeRange.maximum = 2000 # 1000 self.LesionSizeRange.setMinimumValue(self.MinimumLesionSize) self.LesionSizeRange.setMaximumValue(self.MaximumLesionSize) self.LesionSizeRange.connect("minimumValueChanged(double)", self.onMinSizeChanged) self.LesionSizeRange.connect("maximumValueChanged(double)", self.onMaxSizeChanged) parametersFormLayout.addRow("Lesion Size (mm^3)", self.LesionSizeRange) self.LesionSizeRange.setMinimumValue(self.MinimumLesionSize) self.LesionSizeRange.setMaximumValue(self.MaximumLesionSize) # Summary Fields # -------------------------- self.scoreField = dict() for sr in self.summary_reports: self.scoreField[sr] = qt.QLineEdit() self.scoreField[sr].setText(0) parametersFormLayout.addRow("Total " + sr, self.scoreField[sr]) # Update button # ------------- self.updateButton = qt.QPushButton("Update") self.updateButton.toolTip = "Update calcium score computation" self.updateButton.enabled = True self.updateButton.setFixedSize(100, 50) self.updateButton.connect('clicked()', self.onUpdate) # Select table # ------------ self.selectLabels = qt.QTableWidget() self.selectLabels.verticalHeader().hide() self.selectLabels.setColumnCount(6) self.selectLabels.itemClicked.connect(self.handleItemClicked) col_names = [ "", "Agatston Score 2D", "Agatston Score 3D", "Mass Score", "Volume (mm^3)", "Mean HU", "Max HU" ] self.selectLabels.setHorizontalHeaderLabels(col_names) parametersFormLayout.addRow(self.updateButton, self.selectLabels) # Save button # ----------- self.reportsWidget = CaseReportsWidget(self.moduleName, self.columnsDict, parentWidget=self.parent) self.reportsWidget.setup() self.reportsWidget.showPrintButton(False) self.reportsWidget.addObservable( self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) # ROI Area # -------- self.roiCollapsibleButton = ctk.ctkCollapsibleButton() self.roiCollapsibleButton.text = "ROI" self.roiCollapsibleButton.setChecked(False) self.layout.addWidget(self.roiCollapsibleButton) # Layout within the dummy collapsible button roiFormLayout = qt.QFormLayout(self.roiCollapsibleButton) # ROI # --- self.ROIWidget = slicer.qMRMLAnnotationROIWidget() self.roiNode = slicer.vtkMRMLAnnotationROINode() slicer.mrmlScene.AddNode(self.roiNode) self.ROIWidget.setMRMLAnnotationROINode(self.roiNode) roiFormLayout.addRow("", self.ROIWidget) # Add vertical spacer self.layout.addStretch(1) # Add temp nodes self.croppedNode = slicer.vtkMRMLScalarVolumeNode() self.croppedNode.SetHideFromEditors(1) slicer.mrmlScene.AddNode(self.croppedNode) self.labelsNode = slicer.vtkMRMLLabelMapVolumeNode() slicer.mrmlScene.AddNode(self.labelsNode) if self.inputSelector.currentNode(): self.onVolumeChanged(self.inputSelector.currentNode()) def setInteractor(self): slice_views = ["Red", "Yellow", "Green"] for sv in slice_views: slice_view_interactor = slicer.app.layoutManager().sliceWidget( sv).sliceView().renderWindow().GetInteractor() slice_view_interactor.AddObserver("LeftButtonReleaseEvent", self.processEvent) slice_view_interactor.TAG = "three views: %s" % sv slice_view_interactor.Node = slicer.app.layoutManager( ).sliceWidget(sv).sliceLogic().GetLabelLayer().GetSliceNode() def processEvent(self, observee, event): xy = observee.GetEventPosition() transformationMatrix = observee.Node.GetXYToRAS() xyRAS = np.array( transformationMatrix.MultiplyPoint([xy[0], xy[1], 0, 1])) transformationMatrix = vtk.vtkMatrix4x4() self.labelsNode.GetRASToIJKMatrix(transformationMatrix) numpy_data = slicer.util.array(self.labelsNode.GetName()) ijk_coords = transformationMatrix.MultiplyPoint(xyRAS) ijk_coords = np.array(np.rint(ijk_coords), dtype=np.int16) value = numpy_data[ijk_coords[2], ijk_coords[1], ijk_coords[0]] table_item = self.selectLabels.takeItem(value - 1, 0) if table_item.checkState() == qt.Qt.Checked: table_item.setCheckState(qt.Qt.Unchecked) else: table_item.setCheckState(qt.Qt.Checked) self.selectLabels.setItem(value - 1, 0, table_item) self.handleItemClicked(table_item) def computeTotalScore(self): for sr in self.summary_reports: self.totalScores[sr] = 0 for n in range(0, len(self.selectedLabelList)): if self.selectedLabelList[n] == 1: for sr in self.summary_reports: self.totalScores[ sr] = self.totalScores[sr] + self.labelScores[sr][n] for sr in self.summary_reports: self.scoreField[sr].setText(self.totalScores[sr]) def updateModels(self): for n in range(0, len(self.selectedLabelList)): model = self.modelNodes[n] dnode = model.GetDisplayNode() rgb = [1, 0, 0] if self.selectedLabelList[n] == 1: rgb = self.selectedRGB else: ct = slicer.mrmlScene.GetNodeByID( 'vtkMRMLColorTableNodeLabels') ct.GetLookupTable().GetColor(n + 1, rgb) dnode.SetColor(rgb) def deleteModels(self): for m in self.modelNodes: m.SetAndObservePolyData(None) slicer.mrmlScene.RemoveNode(m.GetDisplayNode()) slicer.mrmlScene.RemoveNode(m) self.modelNodes = [] self.selectedLabels = {} def handleItemClicked(self, item): """Select a candidate from list and re-compute the total score""" if item.checkState() == qt.Qt.Checked: self.selectedLabelList[item.row()] = 1 else: self.selectedLabelList[item.row()] = 0 self.computeTotalScore() self.updateModels() def addLabel(self, row, rgb, values): self.selectLabels.setRowCount(row + 1) item0 = qt.QTableWidgetItem('') item0.setFlags(qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsEnabled) item0.setCheckState(qt.Qt.Unchecked) self.selectLabels.setItem(row, 0, item0) for ii, val in enumerate(values): item1 = qt.QTableWidgetItem('') color = qt.QColor() color.setRgbF(rgb[0], rgb[1], rgb[2]) item1.setData(qt.Qt.BackgroundRole, color) item1.setText("%.02f" % val) self.selectLabels.setItem(row, 1 + ii, item1) def computeDensityScore(self, d): score = 0 if d > 129 and d < 200: score = 1 elif d < 300: score = 2 elif d < 400: score = 3 else: score = 4 return score def agatston_computation(self, n, relabelImage, croppedImage, prod_spacing): croppedImage_arr = sitk.GetArrayFromImage(croppedImage) relabelImage_arr = sitk.GetArrayFromImage(relabelImage) ii = np.where(relabelImage_arr == n) min_coord = np.min(ii, axis=1) max_coord = np.max(ii, axis=1) max_coord += 1 # crop_label = (np.array(relabelImage_arr[min_coord[0]:max_coord[0], min_coord[1]:max_coord[1], min_coord[2]:max_coord[2]]) == n) crop_label = relabelImage_arr[min_coord[0]:max_coord[0], min_coord[1]:max_coord[1], min_coord[2]:max_coord[2]] == n crop_img = croppedImage_arr[min_coord[0]:max_coord[0], min_coord[1]:max_coord[1], min_coord[2]:max_coord[2]] crop_img *= (crop_label == 1) agatston = 0 volume = 0 mass_score = 0 for sl in crop_img: max_HU = np.max(sl) size = np.count_nonzero(sl) layer_volume = size * prod_spacing volume += layer_volume agatston += layer_volume * self.computeDensityScore(max_HU) mass_score += layer_volume * np.mean(sl) return agatston, volume, mass_score def statsAsCSV(self, repWidget, volumeNode): if self.totalScores is None: qt.QMessageBox.warning(slicer.util.mainWindow(), "Data not existing", "No statistics calculated") return row = {} row['CaseID'] = volumeNode.GetName() for sr in self.summary_reports: row[sr.replace(" ", "")] = self.totalScores[sr] print row storageNode = self.volumeNode.GetStorageNode() filepath = str(storageNode.GetFullNameFromFileName()) split_path = filepath.split('.') output_path = split_path[0] with open("%s_cac.%s" % (output_path, "json"), 'w') as f: json.dump(row, f) # repWidget.insertRow(**row) # qt.QMessageBox.information(slicer.util.mainWindow(), 'Data saved', 'The data were saved successfully') # def saveSegmentationAsNrrd(self): def saveSegmentation(self): roiCenter = [0, 0, 0] self.roiNode.GetXYZ(roiCenter) transformationMatrix = vtk.vtkMatrix4x4() self.volumeNode.GetRASToIJKMatrix(transformationMatrix) roiCenter_ijk = transformationMatrix.MultiplyPoint( [roiCenter[0], roiCenter[1], roiCenter[2], 1]) roiCenter_ijk = np.array(np.rint(roiCenter_ijk), dtype=np.int16) numpy_data = np.array(slicer.util.array(self.labelsNode.GetName())) lbs = np.argwhere(np.array(self.selectedLabelList) == 1).flatten() + 1 numpy_data[np.logical_not(np.isin(numpy_data, lbs))] = 0 saved_array = np.array(slicer.util.array(self.volumeNode.GetName())) slicer.util.array(self.volumeNode.GetName()).fill(0) slicer.util.array(self.volumeNode.GetName())[ roiCenter_ijk[2] - numpy_data.shape[0] / 2:roiCenter_ijk[2] + (numpy_data.shape[0] + 1) / 2, roiCenter_ijk[1] - numpy_data.shape[1] / 2:roiCenter_ijk[1] + (numpy_data.shape[1] + 1) / 2, roiCenter_ijk[0] - numpy_data.shape[2] / 2:roiCenter_ijk[0] + (numpy_data.shape[2] + 1) / 2] = numpy_data storageNode = self.volumeNode.GetStorageNode() filepath = str(storageNode.GetFullNameFromFileName()) split_path = filepath.split('.') output_path = split_path[0] extension = split_path[1] result = slicer.util.saveNode(self.volumeNode, "%s_cac.%s" % (output_path, extension)) slicer.util.array(self.volumeNode.GetName())[:, :, :] = saved_array def onVolumeChanged(self, value): self.volumeNode = self.inputSelector.currentNode() if self.volumeNode != None: xyz = [0, 0, 0] c = [0, 0, 0] slicer.vtkMRMLSliceLogic.GetVolumeRASBox(self.volumeNode, xyz, c) xyz[:] = [x * 0.2 for x in xyz] self.roiNode.SetXYZ(c) self.roiNode.SetRadiusXYZ(xyz) sp = self.volumeNode.GetSpacing() self.voxelVolume = sp[0] * sp[1] * sp[2] self.sx = sp[0] self.sy = sp[1] self.sz = sp[2] def onMinSizeChanged(self, value): self.MinimumLesionSize = value #self.createModels() def onMaxSizeChanged(self, value): self.MaximumLesionSize = value #self.createModels() def onThresholdMinChanged(self, value): self.ThresholdMin = value #self.createModels() def onThresholdMaxChanged(self, value): self.ThresholdMax = value #self.createModels() def onUpdate(self): self.createModels() def onSaveReport(self): """Save the current values in a persistent csv file""" self.statsAsCSV(self.reportsWidget, self.volumeNode) self.saveSegmentation() def createModels(self): # Reset previous model and labels self.deleteModels() for sr in self.summary_reports: self.labelScores[sr] = [] self.selectedLabelList = [] if self.calcificationType == 0 and self.volumeNode and self.roiNode: slicer.vtkSlicerCropVolumeLogic().CropVoxelBased( self.roiNode, self.volumeNode, self.croppedNode) croppedImage = sitk.ReadImage( sitkUtils.GetSlicerITKReadWriteAddress( self.croppedNode.GetName())) thresholdImage = sitk.BinaryThreshold(croppedImage, self.ThresholdMin, self.ThresholdMax, 1, 0) connectedCompImage = sitk.ConnectedComponent(thresholdImage, True) relabelImage = sitk.RelabelComponent(connectedCompImage) labelStatFilter = sitk.LabelStatisticsImageFilter() labelStatFilter.Execute(croppedImage, relabelImage) if relabelImage.GetPixelID() != sitk.sitkInt16: relabelImage = sitk.Cast(relabelImage, sitk.sitkInt16) sitk.WriteImage( relabelImage, sitkUtils.GetSlicerITKReadWriteAddress( self.labelsNode.GetName())) prod_spacing = np.prod(croppedImage.GetSpacing()) nLabels = labelStatFilter.GetNumberOfLabels() self.totalScore = 0 count = 0 for n in range(0, nLabels): max = labelStatFilter.GetMaximum(n) mean = labelStatFilter.GetMean(n) size = labelStatFilter.GetCount(n) volume = size * self.voxelVolume # current label is discarted if volume not meet the maximum allowed threshold if volume > self.MaximumLesionSize: continue # As ordered, we stop here if the volume of the current label is less than threshold if volume < self.MinimumLesionSize: nLabels = n + 1 break score2d, volume, mass_score = self.agatston_computation( n, relabelImage, croppedImage, prod_spacing) # Agatston 3d: # ----------- density_score = self.computeDensityScore(max) score3d = size * (self.sx * self.sy) * density_score mass_score = mean * volume # self.labelScores["Agatston Score"].append(score) self.labelScores["Agatston Score 3D"].append(score3d) self.labelScores["Agatston Score 2D"].append(score2d) self.labelScores["Mass Score"].append(mass_score) self.labelScores["Volume"].append(volume) self.selectedLabelList.append(0) # generate the contour marchingCubes = vtk.vtkDiscreteMarchingCubes() marchingCubes.SetInputData(self.labelsNode.GetImageData()) marchingCubes.SetValue(0, count + 1) marchingCubes.Update() transformPolyData = vtk.vtkTransformPolyDataFilter() transformPolyData.SetInputData(marchingCubes.GetOutput()) mat = vtk.vtkMatrix4x4() self.labelsNode.GetIJKToRASMatrix(mat) trans = vtk.vtkTransform() trans.SetMatrix(mat) transformPolyData.SetTransform(trans) transformPolyData.Update() poly = vtk.vtkPolyData() poly.DeepCopy(transformPolyData.GetOutput()) modelNode = slicer.vtkMRMLModelNode() slicer.mrmlScene.AddNode(modelNode) dnode = slicer.vtkMRMLModelDisplayNode() slicer.mrmlScene.AddNode(dnode) modelNode.AddAndObserveDisplayNodeID(dnode.GetID()) modelNode.SetAndObservePolyData(poly) ct = slicer.mrmlScene.GetNodeByID( 'vtkMRMLColorTableNodeLabels') rgb = [0, 0, 0] ct.GetLookupTable().GetColor(count + 1, rgb) dnode.SetColor(rgb) # Enable Slice intersection dnode.SetSliceDisplayMode(0) dnode.SetSliceIntersectionVisibility(1) # self.addLabel(count, rgb, [score, mass_score, volume, mean, max]) self.addLabel( count, rgb, [score2d, score3d, mass_score, volume, mean, max]) count = count + 1 self.modelNodes.append(modelNode) self.selectedLabels[poly] = n for sr in self.summary_reports: self.scoreField[sr].setText(self.totalScores[sr])
class CIP_CalciumScoringWidget(ScriptedLoadableModuleWidget): def __init__(self, parent=None): ScriptedLoadableModuleWidget.__init__(self, parent) settings = qt.QSettings() self.developerMode = SlicerUtil.IsDevelopment if not parent: self.parent = slicer.qMRMLWidget() self.parent.setLayout(qt.QVBoxLayout()) self.parent.setMRMLScene(slicer.mrmlScene) else: self.parent = parent self.layout = self.parent.layout() if not parent: self.setup() self.parent.show() self.priority = 2 self.calcificationType = 0 self.ThresholdMin = 130.0 self.ThresholdMax = 1000.0 self.MinimumLesionSize = 1 self.MaximumLesionSize = 500 self.croppedVolumeNode = slicer.vtkMRMLScalarVolumeNode() self.threshImage = vtk.vtkImageData() self.marchingCubes = vtk.vtkDiscreteMarchingCubes() self.transformPolyData = vtk.vtkTransformPolyDataFilter() self.selectedLabelList = [] self.labelScores = [] self.selectedLabels = {} self.modelNodes = [] self.voxelVolume = 1. self.sx = 1. self.sy = 1. self.sz = 1. self.selectedRGB = [1,0,0] self.observerTags = [] self.xy = [] self.summary_reports=["Agatston Score","Mass Score","Volume"] self.labelScores = dict() self.totalScores=dict() for sr in self.summary_reports: self.labelScores[sr]=[] self.totalScores[sr]=0 self.columnsDict = OrderedDict() self.columnsDict["CaseID"] = "CaseID" for sr in self.summary_reports: self.columnsDict[sr.replace(" ","")]=sr def __del__(self): for observee,tag in self.observerTags: observee.RemoveObserver(tag) self.observerTags = [] # def enter(self): # print "Enter" # def exit(self): # print "Exit" def setup(self): # Instantiate and connect widgets ... ScriptedLoadableModuleWidget.setup(self) #self.logic = CIP_CalciumScoringLogic() # # Parameters Area # parametersCollapsibleButton = ctk.ctkCollapsibleButton() parametersCollapsibleButton.text = "Parameters" self.layout.addWidget(parametersCollapsibleButton) # Layout within the dummy collapsible button parametersFormLayout = qt.QFormLayout(parametersCollapsibleButton) # # target volume selector # self.inputSelector = slicer.qMRMLNodeComboBox() self.inputSelector.nodeTypes = ( ("vtkMRMLScalarVolumeNode"), "" ) self.inputSelector.addEnabled = False self.inputSelector.removeEnabled = False self.inputSelector.noneEnabled = False self.inputSelector.showHidden = False self.inputSelector.showChildNodeTypes = False self.inputSelector.setMRMLScene( slicer.mrmlScene ) self.inputSelector.setToolTip( "Pick the input to the algorithm." ) parametersFormLayout.addRow("Target Volume: ", self.inputSelector) self.inputSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onVolumeChanged) self.volumeNode = self.inputSelector.currentNode() # # calcification type # # self.calcificationTypeBox = qt.QComboBox() # self.calcificationTypeBox.addItem("Heart") # self.calcificationTypeBox.addItem("Aorta") # parametersFormLayout.addRow("Region", self.calcificationTypeBox) # self.calcificationTypeBox.connect("currentIndexChanged(int)", self.onTypeChanged) self.ThresholdRange = ctk.ctkRangeWidget() self.ThresholdRange.minimum = 0 self.ThresholdRange.maximum = 2000 self.ThresholdRange.setMinimumValue(self.ThresholdMin) self.ThresholdRange.setMaximumValue(self.ThresholdMax) self.ThresholdRange.connect("minimumValueChanged(double)", self.onThresholdMinChanged) self.ThresholdRange.connect("maximumValueChanged(double)", self.onThresholdMaxChanged) parametersFormLayout.addRow("Threshold Value", self.ThresholdRange) self.ThresholdRange.setMinimumValue(self.ThresholdMin) self.ThresholdRange.setMaximumValue(self.ThresholdMax) self.LesionSizeRange= ctk.ctkRangeWidget() self.LesionSizeRange.minimum = 0.5 self.LesionSizeRange.maximum = 1000 self.LesionSizeRange.setMinimumValue(self.MinimumLesionSize) self.LesionSizeRange.setMaximumValue(self.MaximumLesionSize) self.LesionSizeRange.connect("minimumValueChanged(double)", self.onMinSizeChanged) self.LesionSizeRange.connect("maximumValueChanged(double)", self.onMaxSizeChanged) parametersFormLayout.addRow("Lesion Size (mm^3)", self.LesionSizeRange) self.LesionSizeRange.setMinimumValue(self.MinimumLesionSize) self.LesionSizeRange.setMaximumValue(self.MaximumLesionSize) self.scoreField=dict() for sr in self.summary_reports: self.scoreField[sr] = qt.QLineEdit() self.scoreField[sr].setText(0) parametersFormLayout.addRow("Total "+sr, self.scoreField[sr]) # # Update button and Select Table # self.updateButton = qt.QPushButton("Update") self.updateButton.toolTip = "Update calcium score computation" self.updateButton.enabled = True self.updateButton.setFixedSize(100, 50) #parametersFormLayout.addRow("", self.updateButton) self.updateButton.connect('clicked()', self.onUpdate) # # Select table # self.selectLabels = qt.QTableWidget() #self.selectLabels.horizontalHeader().hide() self.selectLabels.verticalHeader().hide() self.selectLabels.setColumnCount(6) self.selectLabels.itemClicked.connect(self.handleItemClicked) #Add row with columns name col_names=["","Agatston Score","Mass Score","Volume (mm^3)","Mean HU","Max HU"] self.selectLabels.setHorizontalHeaderLabels(col_names) parametersFormLayout.addRow(self.updateButton, self.selectLabels) # # Save Widget Area # #self.saveCollapsibleButton = ctk.ctkCollapsibleButton() #self.saveCollapsibleButton.text = "Saving" #self.layout.addWidget(self.saveCollapsibleButton) self.reportsWidget = CaseReportsWidget(self.moduleName, self.columnsDict, parentWidget=self.parent) self.reportsWidget.setup() self.reportsWidget.showPrintButton(False) self.reportsWidget.addObservable(self.reportsWidget.EVENT_SAVE_BUTTON_CLICKED, self.onSaveReport) # # ROI Area # self.roiCollapsibleButton = ctk.ctkCollapsibleButton() self.roiCollapsibleButton.text = "ROI" self.roiCollapsibleButton.setChecked(False) self.layout.addWidget(self.roiCollapsibleButton) # Layout within the dummy collapsible button roiFormLayout = qt.QFormLayout(self.roiCollapsibleButton) # # ROI # self.ROIWidget = slicer.qMRMLAnnotationROIWidget() self.roiNode = slicer.vtkMRMLAnnotationROINode() slicer.mrmlScene.AddNode(self.roiNode) self.ROIWidget.setMRMLAnnotationROINode(self.roiNode) roiFormLayout.addRow("", self.ROIWidget) #self.roiNode.AddObserver("ModifiedEvent", self.onROIChangedEvent, 1) # Add vertical spacer self.layout.addStretch(1) # Add temp nodes self.croppedNode=slicer.vtkMRMLScalarVolumeNode() self.croppedNode.SetHideFromEditors(1) slicer.mrmlScene.AddNode(self.croppedNode) self.labelsNode=slicer.vtkMRMLLabelMapVolumeNode() slicer.mrmlScene.AddNode(self.labelsNode) if self.inputSelector.currentNode(): self.onVolumeChanged(self.inputSelector.currentNode()) #self.createModels() def cleanup(self): self.reportsWidget.cleanup() self.reportsWidget = None def addLabel(self, row, rgb, values): #print "add row", row, rgb self.selectLabels.setRowCount(row+1) item0 = qt.QTableWidgetItem('') item0.setFlags(qt.Qt.ItemIsUserCheckable | qt.Qt.ItemIsEnabled) item0.setCheckState(qt.Qt.Unchecked) self.selectLabels.setItem(row,0,item0) for ii,val in enumerate(values): item1 = qt.QTableWidgetItem('') color=qt.QColor() color.setRgbF(rgb[0],rgb[1],rgb[2]) item1.setData(qt.Qt.BackgroundRole,color) item1.setText("%.02f"%val) self.selectLabels.setItem(row,1+ii,item1) def handleItemClicked(self, item): if item.checkState() == qt.Qt.Checked: self.selectedLabelList[item.row()] = 1 else: self.selectedLabelList[item.row()] = 0 #print "LIST=", self.selectedLabelList self.computeTotalScore() self.updateModels() def computeTotalScore(self): for sr in self.summary_reports: self.totalScores[sr] = 0 for n in range(0, len(self.selectedLabelList)): if self.selectedLabelList[n] == 1: for sr in self.summary_reports: self.totalScores[sr] = self.totalScores[sr] + self.labelScores[sr][n] for sr in self.summary_reports: self.scoreField[sr].setText(self.totalScores[sr]) def updateModels(self): for n in range(0, len(self.selectedLabelList)): model = self.modelNodes[n] dnode = model.GetDisplayNode() rgb = [1,0,0] if self.selectedLabelList[n] == 1: rgb = self.selectedRGB else: ct=slicer.mrmlScene.GetNodeByID('vtkMRMLColorTableNodeLabels') ct.GetLookupTable().GetColor(n+1,rgb) dnode.SetColor(rgb) def setInteractor(self): self.renderWindow = slicer.app.layoutManager().threeDWidget(0).threeDView().renderWindow() self.iren = self.renderWindow.GetInteractor() lm = slicer.app.layoutManager() for v in range(lm.threeDViewCount): td = lm.threeDWidget(v) ms = vtk.vtkCollection() td.getDisplayableManagers(ms) for i in range(ms.GetNumberOfItems()): m = ms.GetItemAsObject(i) if m.GetClassName() == "vtkMRMLModelDisplayableManager": self.dispManager = m break self.propPicker = vtk.vtkPropPicker() self.iren.SetPicker(self.propPicker) #self.propPicker.AddObserver("EndPickEvent", self.PickProp) #self.propPicker.AddObserver("PickEvent", self.PickProp) self.renderer = slicer.app.layoutManager().threeDWidget(0).threeDView().renderWindow().GetRenderers().GetFirstRenderer() tag = self.iren.AddObserver("LeftButtonReleaseEvent", self.processEvent, self.priority) self.observerTags.append([self.iren,tag]) tag = self.iren.AddObserver("MouseMoveEvent", self.processEvent, self.priority) self.observerTags.append([self.iren,tag]) self.mouseInteractor = MouseInteractorActor() self.mouseInteractor.SetDefaultRenderer(self.renderer) self.iterStyleSave = iren.GetInteractorStyle() self.iren.SetInteractorStyle(self.mouseInteractor) def onVolumeChanged(self, value): self.volumeNode = self.inputSelector.currentNode() if self.volumeNode != None: xyz = [0,0,0] c=[0,0,0] slicer.vtkMRMLSliceLogic.GetVolumeRASBox(self.volumeNode,xyz,c) xyz[:]=[x*0.2 for x in xyz] self.roiNode.SetXYZ(c) self.roiNode.SetRadiusXYZ(xyz) sp = self.volumeNode.GetSpacing() self.voxelVolume = sp[0]*sp[1]*sp[2] self.sx=sp[0] self.sy=sp[1] self.sz=sp[2] #self.createModels() def onTypeChanged(self, value): self.calcificationType = value if self.calcificationType == 0: self.roiCollapsibleButton.setEnabled(1) self.ROIWidget.setEnabled(1) self.roiNode.SetDisplayVisibility(1) #self.logic.cropVolumeWithROI(self.volumeNode, self.roiNode, self.croppedVolumeNode) else: self.roiCollapsibleButton.setEnabled(0) self.ROIWidget.setEnabled(0) self.roiNode.SetDisplayVisibility(0) #self.createModels() def onMinSizeChanged(self, value): self.MinimumLesionSize = value #self.createModels() def onMaxSizeChanged(self, value): self.MaximumLesionSize = value #self.createModels() def onThresholdMinChanged(self, value): self.ThresholdMin = value #self.createModels() def onThresholdMaxChanged(self, value): self.ThresholdMax = value #self.createModels() def onROIChangedEvent(self, observee, event): pass #self.createModels() def onUpdate(self): self.createModels() def deleteModels(self): for m in self.modelNodes: m.SetAndObservePolyData(None) slicer.mrmlScene.RemoveNode(m.GetDisplayNode()) slicer.mrmlScene.RemoveNode(m) self.modelNodes = [] self.selectedLabels = {} def PickProp(self, object, event): # print "PICK" pickedActor = self.propPicker.GetActor() poly = pickedActor.GetMapper().GetInput() label = self.selectedLabels[poly] print ("picked label = ", label) def processEvent(self,observee,event): # print "PICK EVENT", event self.xy = self.iren.GetEventPosition() self.propPicker.PickProp(self.xy[0], self.xy[1], self.renderer) pickedActor = self.propPicker.GetActor() if pickedActor: poly = pickedActor.GetMapper().GetInput() label = self.selectedLabels[poly] print ("picked label = ", label) def onSaveReport(self): """ Save the current values in a persistent csv file """ self.statsAsCSV(self.reportsWidget, self.volumeNode) def statsAsCSV(self, repWidget, volumeNode): if self.totalScores is None: qt.QMessageBox.warning(slicer.util.mainWindow(), "Data not existing", "No statistics calculated") return row={} row['CaseID']=volumeNode.GetName() for sr in self.summary_reports: row[sr.replace(" ","")]=self.totalScores[sr] repWidget.insertRow(**row) qt.QMessageBox.information(slicer.util.mainWindow(), 'Data saved', 'The data were saved successfully') def computeDensityScore(self, d): score = 0 if d > 129 and d < 200: score = 1 elif d < 300: score = 2 elif d < 400: score = 3 else: score = 4 return score def createModels(self): self.deleteModels() for sr in self.summary_reports: self.labelScores[sr]=[] self.selectedLabelList = [] if self.calcificationType == 0 and self.volumeNode and self.roiNode: #print 'in Heart Create Models' slicer.vtkSlicerCropVolumeLogic().CropVoxelBased(self.roiNode, self.volumeNode, self.croppedNode) croppedImage = sitk.ReadImage( sitkUtils.GetSlicerITKReadWriteAddress(self.croppedNode.GetName())) thresholdImage = sitk.BinaryThreshold(croppedImage,self.ThresholdMin, self.ThresholdMax, 1, 0) connectedCompImage =sitk.ConnectedComponent(thresholdImage, True) relabelImage =sitk.RelabelComponent(connectedCompImage) labelStatFilter =sitk.LabelStatisticsImageFilter() labelStatFilter.Execute(croppedImage, relabelImage) if relabelImage.GetPixelID() != sitk.sitkInt16: relabelImage = sitk.Cast( relabelImage, sitk.sitkInt16 ) sitk.WriteImage( relabelImage, sitkUtils.GetSlicerITKReadWriteAddress(self.labelsNode.GetName())) nLabels = labelStatFilter.GetNumberOfLabels() #print "Number of labels = ", nLabels self.totalScore = 0 count = 0 #Computation of the score follows this paper: #C. H McCollough, Radiology, 243(2), 2007 for n in range(0,nLabels): max = labelStatFilter.GetMaximum(n) mean = labelStatFilter.GetMean(n) size = labelStatFilter.GetCount(n) volume = size*self.voxelVolume if volume > self.MaximumLesionSize: continue if volume < self.MinimumLesionSize: nLabels = n+1 break density_score = self.computeDensityScore(max) #Agatston score is \sum_i area_i * density_score_i #For now we assume that all the plaques have the same density score score = size*(self.sx*self.sy)*density_score mass_score = mean*volume #print "label = ", n, " max = ", max, " score = ", score, " voxels = ", size self.labelScores["Agatston Score"].append(score) self.labelScores["Mass Score"].append(mass_score) self.labelScores["Volume"].append(volume) self.selectedLabelList.append(0) self.marchingCubes.SetInputData(self.labelsNode.GetImageData()) self.marchingCubes.SetValue(0, n) self.marchingCubes.Update() self.transformPolyData.SetInputData(self.marchingCubes.GetOutput()) mat = vtk.vtkMatrix4x4() self.labelsNode.GetIJKToRASMatrix(mat) trans = vtk.vtkTransform() trans.SetMatrix(mat) self.transformPolyData.SetTransform(trans) self.transformPolyData.Update() poly = vtk.vtkPolyData() poly.DeepCopy(self.transformPolyData.GetOutput()) modelNode = slicer.vtkMRMLModelNode() slicer.mrmlScene.AddNode(modelNode) dnode = slicer.vtkMRMLModelDisplayNode() slicer.mrmlScene.AddNode(dnode) modelNode.AddAndObserveDisplayNodeID(dnode.GetID()) modelNode.SetAndObservePolyData(poly) ct=slicer.mrmlScene.GetNodeByID('vtkMRMLColorTableNodeLabels') rgb = [0,0,0] ct.GetLookupTable().GetColor(count+1,rgb) dnode.SetColor(rgb) #Enable Slice intersection dnode.SetSliceDisplayMode(0) dnode.SetSliceIntersectionVisibility(1) self.addLabel(count, rgb, [score,mass_score,volume,mean,max]) count = count+1 self.modelNodes.append(modelNode) self.selectedLabels[poly] = n #a = slicer.util.array(tn.GetID()) #sa = sitk.GetImageFromArray(a) for sr in self.summary_reports: self.scoreField[sr].setText(self.totalScores[sr]) else: print ("not implemented")
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()