diff --git a/ASO_CBCT/LinearTransform_t.tfm b/ASO_CBCT/LinearTransform_t.tfm new file mode 100644 index 0000000..de1d214 --- /dev/null +++ b/ASO_CBCT/LinearTransform_t.tfm @@ -0,0 +1,5 @@ +#Insight Transform File V1.0 +#Transform 0 +Transform: AffineTransform_double_3_3 +Parameters: 0.9763290732702393 0.15616745925611628 0.14964379491567228 -0.1421477793365056 0.9847545451025459 -0.10026213008697676 -0.16302008930489087 0.07661729941316797 0.9836433499565058 3.8520097928318386 10.915991569159537 -4.582919224464414 +FixedParameters: 0 0 0 diff --git a/MRI2CBCT/MRI2CBCT.py b/MRI2CBCT/MRI2CBCT.py index 99b6911..0e96582 100644 --- a/MRI2CBCT/MRI2CBCT.py +++ b/MRI2CBCT/MRI2CBCT.py @@ -1,10 +1,12 @@ import logging import os from typing import Annotated, Optional +from qt import QApplication, QWidget, QTableWidget, QTableWidgetItem, QHeaderView,QSpinBox, QVBoxLayout, QLabel, QSizePolicy, QCheckBox, QFileDialog import vtk import slicer +from functools import partial from slicer.i18n import tr as _ from slicer.i18n import translate from slicer.ScriptedLoadableModule import * @@ -139,6 +141,8 @@ def __init__(self, parent=None) -> None: ScriptedLoadableModuleWidget.__init__(self, parent) VTKObservationMixin.__init__(self) # needed for parameter node observation self.logic = None + self.checked_cells = set() + self.minus_checked_rows = set() self._parameterNode = None self._parameterNodeGuiTag = None @@ -162,16 +166,249 @@ def setup(self) -> None: self.logic = MRI2CBCTLogic() # Connections - + # LineEditOutputReg # These connections ensure that we update parameter node when scene is closed self.addObserver(slicer.mrmlScene, slicer.mrmlScene.StartCloseEvent, self.onSceneStartClose) self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) # Buttons self.ui.applyButton.connect("clicked(bool)", self.onApplyButton) + self.ui.SearchButtonCBCT.connect("clicked(bool)",partial(self.openFinder,"InputCBCT")) + self.ui.SearchButtonMRI.connect("clicked(bool)",partial(self.openFinder,"InputMRI")) + self.ui.SearchButtonRegMRI.connect("clicked(bool)",partial(self.openFinder,"InputRegMRI")) + self.ui.SearchButtonRegCBCT.connect("clicked(bool)",partial(self.openFinder,"InputRegCBCT")) + self.ui.SearchButtonRegLabel.connect("clicked(bool)",partial(self.openFinder,"InputRegLabel")) + self.ui.SearchOutputFolderOrientCBCT.connect("clicked(bool)",partial(self.openFinder,"OutputOrientCBCT")) + self.ui.SearchOutputFolderOrientMRI.connect("clicked(bool)",partial(self.openFinder,"OutputOrientMRI")) + self.ui.SearchOutputFolderResample.connect("clicked(bool)",partial(self.openFinder,"OutputOrientResample")) + self.ui.SearchButtonOutput.connect("clicked(bool)",partial(self.openFinder,"OutputReg")) # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() + + self.ui.outputCollapsibleButton.setText("Registration") + self.ui.inputsCollapsibleButton.setText("Preprocess") + + self.ui.outputCollapsibleButton.setChecked(True) # True to expand, False to collapse + self.ui.inputsCollapsibleButton.setChecked(False) + ################################################################################################## + ### Orientation Table + self.tableWidgetOrient = self.ui.tableWidgetOrient + self.tableWidgetOrient.setRowCount(3) # Rows for New Direction X, Y, Z + self.tableWidgetOrient.setColumnCount(4) # Columns for X, Y, Z, and Minus + + # Set the headers + self.tableWidgetOrient.setHorizontalHeaderLabels(["X", "Y", "Z", "Negative"]) + self.tableWidgetOrient.setVerticalHeaderLabels(["New Direction X", "New Direction Y", "New Direction Z"]) + + # Set the horizontal header to stretch and fill the available space + header = self.tableWidgetOrient.horizontalHeader() + header.setSectionResizeMode(QHeaderView.Stretch) + + # Set a fixed height for the table to avoid stretching + self.tableWidgetOrient.setFixedHeight(self.tableWidgetOrient.horizontalHeader().height + + self.tableWidgetOrient.verticalHeader().sectionSize(0) * self.tableWidgetOrient.rowCount) + + # Add widgets for each cell + for row in range(3): + for col in range(4): # Columns X, Y, Z, and Minus + if col!=3 : + checkBox = QCheckBox('0') + checkBox.stateChanged.connect(lambda state, r=row, c=col: self.onCheckboxOrientClicked(r, c, state)) + self.tableWidgetOrient.setCellWidget(row, col, checkBox) + else : + checkBox = QCheckBox('No') + checkBox.stateChanged.connect(lambda state, r=row, c=col: self.onCheckboxOrientClicked(r, c, state)) + self.tableWidgetOrient.setCellWidget(row, col, checkBox) + + self.ui.ButtonDefaultOrientMRI.connect("clicked(bool)",self.defaultOrientMRI) + + ################################################################################################## + ### Normalization Table + self.tableWidgetNorm = self.ui.tableWidgetNorm + + self.tableWidgetNorm.setRowCount(2) # MRI and CBCT rows + header row + self.tableWidgetNorm.setColumnCount(4) # Min, Max for Normalization and Percentile + + # Set the horizontal header to stretch and fill the available space + header = self.tableWidgetNorm.horizontalHeader() + header.setSectionResizeMode(QHeaderView.Stretch) + + # Set a fixed height for the table to avoid stretching + self.tableWidgetNorm.setFixedHeight(self.tableWidgetNorm.horizontalHeader().height + + self.tableWidgetNorm.verticalHeader().sectionSize(0) * self.tableWidgetNorm.rowCount) + + # Set the headers + self.tableWidgetNorm.setHorizontalHeaderLabels(["Normalization Min", "Normalization Max", "Percentile Min", "Percentile Max"]) + self.tableWidgetNorm.setVerticalHeaderLabels([ "MRI", "CBCT"]) + + + for row in range(2): + for col in range(4): + spinBox = QSpinBox() + spinBox.setMaximum(10000) + self.tableWidgetNorm.setCellWidget(row, col, spinBox) + + self.ui.ButtonCheckBoxDefaultNorm1.connect("clicked(bool)",partial(self.DefaultNorm,"1")) + self.ui.ButtonCheckBoxDefaultNorm2.connect("clicked(bool)",partial(self.DefaultNorm,"2")) + + ################################################################################################## + ### Resample Table + self.tableWidgetResample = self.ui.tableWidgetResample + + self.tableWidgetResample.setRowCount(1) # MRI and CBCT rows + header row + self.tableWidgetResample.setColumnCount(3) # Min, Max for Normalization and Percentile + + # Set the horizontal header to stretch and fill the available space + header = self.tableWidgetResample.horizontalHeader() + header.setSectionResizeMode(QHeaderView.Stretch) + + # Set a fixed height for the table to avoid stretching + self.tableWidgetResample.setFixedHeight(self.tableWidgetResample.horizontalHeader().height + + self.tableWidgetResample.verticalHeader().sectionSize(0) * self.tableWidgetResample.rowCount) + + # Set the headers + self.tableWidgetResample.setHorizontalHeaderLabels(["X", "Y", "Z"]) + self.tableWidgetResample.setVerticalHeaderLabels([ "Number of slices"]) + + + spinBox = QSpinBox() + spinBox.setMaximum(10000) + spinBox.setValue(119) + self.tableWidgetResample.setCellWidget(0, 0, spinBox) + + spinBox = QSpinBox() + spinBox.setMaximum(10000) + spinBox.setValue(443) + self.tableWidgetResample.setCellWidget(0, 1, spinBox) + + spinBox = QSpinBox() + spinBox.setMaximum(10000) + spinBox.setValue(443) + self.tableWidgetResample.setCellWidget(0, 2, spinBox) + + + def onCheckboxOrientClicked(self, row, col, state): + if col == 3: # If the "Minus" column checkbox is clicked + if state == 2: # Checkbox is checked + self.minus_checked_rows.add(row) + checkBox = self.tableWidgetOrient.cellWidget(row, col) + checkBox.setText('Yes') + for c in range(3): + checkBox = self.tableWidgetOrient.cellWidget(row, c) + if checkBox.text=="1": + checkBox.setText('-1') + else: # Checkbox is unchecked + self.minus_checked_rows.discard(row) + checkBox = self.tableWidgetOrient.cellWidget(row, col) + checkBox.setText('No') + for c in range(3): + checkBox = self.tableWidgetOrient.cellWidget(row, c) + if checkBox.text=="-1": + checkBox.setText('1') + else : + if state == 2: # Checkbox is checked + # Set the clicked checkbox to '1' and uncheck all others in the same row + for c in range(3): + checkBox = self.tableWidgetOrient.cellWidget(row, c) + if checkBox: + if c == col: + if row in self.minus_checked_rows: + checkBox.setText('-1') + else : + checkBox.setText('1') + checkBox.setStyleSheet("color: black;") + checkBox.setStyleSheet("font-weight: bold;") + self.checked_cells.add((row, col)) + else: + checkBox.setText('0') + checkBox.setChecked(False) + self.checked_cells.discard((row, c)) + + # Check for other '1' in the same column and set them to '0' + for r in range(3): + if r != row: + checkBox = self.tableWidgetOrient.cellWidget(r, col) + if checkBox and (checkBox.text == '1' or checkBox.text == '-1'): + checkBox.setText('0') + checkBox.setChecked(False) + checkBox.setStyleSheet("color: gray;") + checkBox.setStyleSheet("font-weight: normal;") + self.checked_cells.discard((r, col)) + + # Check if two checkboxes are checked in different rows, then check the third one + if len(self.checked_cells) == 2: + all_rows = {0, 1, 2} + all_cols = {0, 1, 2} + checked_rows = {r for r, c in self.checked_cells} + unchecked_row = list(all_rows - checked_rows)[0] + + # Find the unchecked column + unchecked_cols = list(all_cols - {c for r, c in self.checked_cells}) + # print("unchecked_cols : ",unchecked_cols) + for c in range(3): + checkBox = self.tableWidgetOrient.cellWidget(unchecked_row, c) + if c in unchecked_cols: + checkBox.setStyleSheet("color: black;") + checkBox.setStyleSheet("font-weight: bold;") + checkBox.setChecked(True) + if unchecked_row in self.minus_checked_rows: + checkBox.setText('-1') + else : + checkBox.setText('1') + self.checked_cells.add((unchecked_row, c)) + else : + checkBox.setText('0') + checkBox.setChecked(False) + self.checked_cells.discard((row, c)) + + else: # Checkbox is unchecked + checkBox = self.tableWidgetOrient.cellWidget(row, col) + if checkBox: + checkBox.setText('0') + checkBox.setStyleSheet("color: black;") + checkBox.setStyleSheet("font-weight: normal;") + self.checked_cells.discard((row, col)) + + # Reset the style of all checkboxes in the same row + for c in range(3): + checkBox = self.tableWidgetOrient.cellWidget(row, c) + if checkBox: + checkBox.setStyleSheet("color: black;") + checkBox.setStyleSheet("font-weight: normal;") + + def getCheckboxValuesOrient(self): + values = [] + for row in range(3): + for col in range(3): + checkBox = self.tableWidgetOrient.cellWidget(row, col) + if checkBox: + values.append(int(checkBox.text)) + return tuple(values) + + def defaultOrientMRI(self): + initial_states = [ + (0, 2, -1), + (1, 0, 1), + (2, 1, -1) + ] + for row, col, value in initial_states: + checkBox = self.tableWidgetOrient.cellWidget(row, col) + if checkBox: + if value == 1: + checkBox.setChecked(True) + checkBox.setText('1') + checkBox.setStyleSheet("font-weight: bold;") + self.checked_cells.add((row, col)) + elif value == -1: + checkBox.setChecked(True) + checkBox.setText('-1') + checkBox.setStyleSheet("font-weight: bold;") + minus_checkBox = self.tableWidgetOrient.cellWidget(row, 3) + if minus_checkBox: + minus_checkBox.setChecked(True) + minus_checkBox.setText("Yes") + self.minus_checked_rows.add(row) def cleanup(self) -> None: """Called when the application closes and the module widget is destroyed.""" @@ -193,7 +430,7 @@ def exit(self) -> None: def onSceneStartClose(self, caller, event) -> None: """Called just before the scene is closed.""" # Parameter node will be reset, do not use it anymore - self.setParameterNode(None) + pass def onSceneEndClose(self, caller, event) -> None: """Called just after the scene is closed.""" @@ -206,51 +443,116 @@ def initializeParameterNode(self) -> None: # Parameter node stores all user choices in parameter values, node selections, etc. # so that when the scene is saved and reloaded, these settings are restored. - self.setParameterNode(self.logic.getParameterNode()) + # self.setParameterNode(self.logic.getParameterNode()) # Select default input nodes if nothing is selected yet to save a few clicks for the user - if not self._parameterNode.inputVolume: - firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") - if firstVolumeNode: - self._parameterNode.inputVolume = firstVolumeNode + # if not self._parameterNode.inputVolume: + # firstVolumeNode = slicer.mrmlScene.GetFirstNodeByClass("vtkMRMLScalarVolumeNode") + # if firstVolumeNode: + # self._parameterNode.inputVolume = firstVolumeNode + pass + - def setParameterNode(self, inputParameterNode: Optional[MRI2CBCTParameterNode]) -> None: + def _checkCanApply(self, caller=None, event=None) -> None: + pass + + def getNormalization(self): + values = [] + for row in range(self.tableWidgetNorm.rowCount): + rowData = [] + for col in range(self.tableWidgetNorm.columnCount): + widget = self.tableWidgetNorm.cellWidget(row, col) + if isinstance(widget, QSpinBox): + rowData.append(widget.value) + values.append(rowData) + return(values) + + def DefaultNorm(self,num : str,_)->None: + # Define the default values for each cell + if num=="1": + default_values = [ + [0, 100, 0, 100], + [0, 75, 10, 95] + ] + else : + default_values = [ + [0, 100, 10, 95], + [0, 100, 10, 95] + ] + + for row in range(self.tableWidgetNorm.rowCount): + for col in range(self.tableWidgetNorm.columnCount): + spinBox = QSpinBox() + spinBox.setMaximum(10000) + spinBox.setValue(default_values[row][col]) + self.tableWidgetNorm.setCellWidget(row, col, spinBox) + + def openFinder(self,nom : str,_) -> None : """ - Set and observe parameter node. - Observation is needed because when the parameter node is changed then the GUI must be updated immediately. + Open finder to let the user choose is files or folder """ + if nom=="InputMRI": + print("self.ui.ComboBoxMRI.currentIndex : ",self.ui.ComboBoxMRI.currentIndex) + print("Type de self.ui.ComboBoxMRI.currentIndex : ", type(self.ui.ComboBoxMRI.currentIndex)) + print("self.ui.ComboBoxMRI.currentIndex : ",self.ui.ComboBoxMRI.currentIndex==1) + if self.ui.ComboBoxMRI.currentIndex==1: + print("oui") + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + else : + surface_folder = QFileDialog.getOpenFileName(self.parent,'Open a file',) + + self.ui.LineEditMRI.setText(surface_folder) + + elif nom=="InputCBCT": + if self.ui.ComboBoxCBCT.currentIndex==1: + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + else : + surface_folder = QFileDialog.getOpenFileName(self.parent,'Open a file',) + self.ui.LineEditCBCT.setText(surface_folder) + + elif nom=="InputRegCBCT": + if self.ui.comboBoxRegCBCT.currentIndex==1: + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + else : + surface_folder = QFileDialog.getOpenFileName(self.parent,'Open a file',) + self.ui.lineEditRegCBCT.setText(surface_folder) + + elif nom=="InputRegMRI": + if self.ui.comboBoxRegMRI.currentIndex==1: + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + else : + surface_folder = QFileDialog.getOpenFileName(self.parent,'Open a file',) + self.ui.lineEditRegMRI.setText(surface_folder) + + elif nom=="InputRegLabel": + if self.ui.comboBoxRegLabel.currentIndex==1: + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + else : + surface_folder = QFileDialog.getOpenFileName(self.parent,'Open a file',) + self.ui.lineEditRegLabel.setText(surface_folder) + + + elif nom=="OutputOrientCBCT": + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + self.ui.lineEditOutputOrientCBCT.setText(surface_folder) + + elif nom=="OutputOrientMRI": + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + self.ui.lineEditOutputOrientMRI.setText(surface_folder) + + elif nom=="OutputOrientResample": + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + self.ui.lineEditOuputResample.setText(surface_folder) + + elif nom=="OutputReg": + surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") + self.ui.LineEditOutput.setText(surface_folder) - if self._parameterNode: - self._parameterNode.disconnectGui(self._parameterNodeGuiTag) - self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self._checkCanApply) - self._parameterNode = inputParameterNode - if self._parameterNode: - # Note: in the .ui file, a Qt dynamic property called "SlicerParameterName" is set on each - # ui element that needs connection. - self._parameterNodeGuiTag = self._parameterNode.connectGui(self.ui) - self.addObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self._checkCanApply) - self._checkCanApply() - - def _checkCanApply(self, caller=None, event=None) -> None: - if self._parameterNode and self._parameterNode.inputVolume and self._parameterNode.thresholdedVolume: - self.ui.applyButton.toolTip = _("Compute output volume") - self.ui.applyButton.enabled = True - else: - self.ui.applyButton.toolTip = _("Select input and output volume nodes") - self.ui.applyButton.enabled = False def onApplyButton(self) -> None: """Run processing when user clicks "Apply" button.""" - with slicer.util.tryWithErrorDisplay(_("Failed to compute results."), waitCursor=True): - # Compute output - self.logic.process(self.ui.inputSelector.currentNode(), self.ui.outputSelector.currentNode(), - self.ui.imageThresholdSliderWidget.value, self.ui.invertOutputCheckBox.checked) - - # Compute inverted output (if needed) - if self.ui.invertedOutputSelector.currentNode(): - # If additional output volume is selected then result with inverted threshold is written there - self.logic.process(self.ui.inputSelector.currentNode(), self.ui.invertedOutputSelector.currentNode(), - self.ui.imageThresholdSliderWidget.value, not self.ui.invertOutputCheckBox.checked, showResult=False) + print("get_normalization : ",self.getNormalization()) + print("getCheckboxValuesOrient : ",self.getCheckboxValuesOrient()) # diff --git a/MRI2CBCT/Resources/UI/MRI2CBCT.ui b/MRI2CBCT/Resources/UI/MRI2CBCT.ui index 5e8a630..843e937 100644 --- a/MRI2CBCT/Resources/UI/MRI2CBCT.ui +++ b/MRI2CBCT/Resources/UI/MRI2CBCT.ui @@ -6,8 +6,8 @@ 0 0 - 462 - 725 + 535 + 1210 @@ -21,39 +21,22 @@ - - - - Search - - - - - - - + Search - - - - Input CBCT file(s) : - - - - - + + - Input MRI files(s): + Search - - + + File @@ -66,8 +49,25 @@ - - + + + + Input MRI files(s): + + + + + + + + + + Input CBCT file(s) : + + + + + File @@ -81,19 +81,201 @@ - - - - - - Mirror - - + + + + + _________________________________________________________ + + + + + + + Orientation + Segmentation of the CBCT + + + + + + + + + Output folder : + + + + + + + Search + + + + + + + + + + + + + + + + + + Orient and Segment CBCT + + + + + + + ______________________________________________________________________________________________ + + + + + + + Orientation + centering of MRI + + + + + + + + + + + + + + + + Default + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Output folder : + + + + + + + + + + Search + + + + + + + + + Orient and centering MRI + + + + + + + ______________________________________________________________________________________________ + + + + + + + Resample + + + + + + + + + + + + Output folder : + + + + + + + + + + Search + + + + + + + + + + + + MRI & CBCT + + + + + MRI + + + + + CBCT + + + + + + + + Run resample + + + + + @@ -103,68 +285,192 @@ Output - + + + + - + Search - + + + + Output folder : - - - - + - _apply + _reg - + Suffix : - - - - AutoFill - - - + + + + + + Default 1 + + + + + + + Default 2 + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + Input CBCT : + + + + + + + + + + + + + Input MRI : + + + + + + + Search + + + + + + + + + + Search + + + + + + + Input Seg CBCT : + + + + + + + Search + + + + + + + + File + + + + + Folder + + + + + + + + + File + + + + + Folder + + + + + + + + + File + + + + + Folder + + + + + + + + + + true + + + Run the algorithm. + + + Registration + + + - - - - false - - - Run the algorithm. - - - Apply matrix - - -