Beispiel #1
0
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
Beispiel #2
0
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
Beispiel #3
0
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])
Beispiel #4
0
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")
Beispiel #5
0
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()