From c5b00983cebfc222d7530425b89926c04f8d897d Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Tue, 11 Jun 2024 16:45:51 -0400 Subject: [PATCH 1/9] ENH : add nrrd files --- AutoMatrix/AutoMatrix.py | 14 +++++++------- AutoMatrix/Method/General_tools.py | 7 +++++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/AutoMatrix/AutoMatrix.py b/AutoMatrix/AutoMatrix.py index 0842385..207feac 100644 --- a/AutoMatrix/AutoMatrix.py +++ b/AutoMatrix/AutoMatrix.py @@ -468,7 +468,7 @@ def ProcessVolume(self)-> None: print("not a .nii.gz") self.UpdateTime() - if extension_scan!=".nii.gz": + if extension_scan!=".nii.gz" and extension_scan!=".nrrd": model = slicer.util.loadModel(scan) else : model = slicer.util.loadVolume(scan) @@ -616,7 +616,7 @@ def onProcessStarted(self)->None: Initialize the variables and progress bar. """ if os.path.isdir(self.ui.LineEditPatient.text): - self.nbFiles = len(self.dico_patient[".vtk"]) + len(self.dico_patient['.vtp']) + len(self.dico_patient['.stl']) + len(self.dico_patient['.off']) + len(self.dico_patient['.obj']) + len(self.dico_patient['.nii.gz']) + self.nbFiles = len(self.dico_patient[".vtk"]) + len(self.dico_patient['.vtp']) + len(self.dico_patient['.stl']) + len(self.dico_patient['.off']) + len(self.dico_patient['.obj']) + len(self.dico_patient['.nii.gz']) + len(self.dico_patient['nrrd']) else: self.nbFiles = 1 self.ui.progressBar.setValue(0) @@ -645,10 +645,10 @@ def CheckGoodEntre(self)->bool: warning_text = warning_text + "Enter file patient" + "\n" else : if self.ui.ComboBoxPatient.currentIndex==1 : #folder option - self.dico_patient=search(self.ui.LineEditPatient.text,'.vtk','.vtp','.stl','.off','.obj','.nii.gz') - if len(self.dico_patient['.vtk'])==0 and len(self.dico_patient['.vtp']) and len(self.dico_patient['.stl']) and len(self.dico_patient['.off']) and len(self.dico_patient['.obj']) and len(self.dico_patient['.nii.gz']) : + self.dico_patient=search(self.ui.LineEditPatient.text,'.vtk','.vtp','.stl','.off','.obj','.nii.gz','nrrd') + if len(self.dico_patient['.vtk'])==0 and len(self.dico_patient['.vtp']) and len(self.dico_patient['.stl']) and len(self.dico_patient['.off']) and len(self.dico_patient['.obj']) and len(self.dico_patient['.nii.gz']) and len(self.dico_patient['.nrrd']) : warning_text = warning_text + "Folder empty or wrong type of file patient" + "\n" - warning_text = warning_text + "File authorized : .vtk / .vtp / .stl / .off / .obj / .nii.gz" + "\n" + warning_text = warning_text + "File authorized : .vtk / .vtp / .stl / .off / .obj / .nii.gz / .nrrd" + "\n" elif self.ui.ComboBoxPatient.currentIndex==0 : # file option fname, extension = os.path.splitext(os.path.basename(self.ui.LineEditPatient.text)) try : @@ -656,9 +656,9 @@ def CheckGoodEntre(self)->bool: extension = extension2+extension except : print("not a .nii.gz") - if extension != ".vtk" and extension != ".vtp" and extension != ".stl" and extension != ".off" and extension != ".obj" and extension != ".nii.gz" : + if extension != ".vtk" and extension != ".vtp" and extension != ".stl" and extension != ".off" and extension != ".obj" and extension != ".nii.gz" and extension != ".nrrd": warning_text = warning_text + "Wrong type of file patient detected" + "\n" - warning_text = warning_text + "File authorized : .vtk / .vtp / .stl / .off / .obj / .nii.gz" + "\n" + warning_text = warning_text + "File authorized : .vtk / .vtp / .stl / .off / .obj / .nii.gz / .nrrd" + "\n" if self.ui.LineEditMatrix.text=="": diff --git a/AutoMatrix/Method/General_tools.py b/AutoMatrix/Method/General_tools.py index efd0c37..4c419ab 100644 --- a/AutoMatrix/Method/General_tools.py +++ b/AutoMatrix/Method/General_tools.py @@ -45,7 +45,7 @@ def GetPatients(file_path:str,matrix_path:str): files = [] if Path(file_path).is_dir(): - files_original = search(file_path,'.vtk','.vtp','.stl','.off','.obj','.nii.gz') + files_original = search(file_path,'.vtk','.vtp','.stl','.off','.obj','.nii.gz','.nrrd') files = [] for i in range(len(files_original['.vtk'])): files.append(files_original['.vtk'][i]) @@ -64,6 +64,9 @@ def GetPatients(file_path:str,matrix_path:str): for i in range(len(files_original['.nii.gz'])): files.append(files_original['.nii.gz'][i]) + + for i in range(len(files_original['.nrrd'])): + files.append(files_original['.nrrd'][i]) for i in range(len(files)): file = files[i] @@ -87,7 +90,7 @@ def GetPatients(file_path:str,matrix_path:str): except : print("not a .nii.gz") - if extension ==".vtk" or extension ==".vtp" or extension ==".stl" or extension ==".off" or extension ==".obj" or extension==".nii.gz" : + if extension ==".vtk" or extension ==".vtp" or extension ==".stl" or extension ==".off" or extension ==".obj" or extension==".nii.gz" or extension==".nrrd" : files = [file_path] file_pat = os.path.basename(file_path).split('_Seg')[0].split('_seg')[0].split('_Scan')[0].split('_scan')[0].split('_Or')[0].split('_OR')[0].split('_MAND')[0].split('_MD')[0].split('_MAX')[0].split('_MX')[0].split('_CB')[0].split('_lm')[0].split('_T2')[0].split('_T1')[0].split('_Cl')[0].split('.')[0].replace('.','') for i in range(50): From daf66111cd586f29601b901af93599699c6b756b Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Wed, 3 Jul 2024 09:53:30 -0400 Subject: [PATCH 2/9] STYLE : just add files nothing working --- CMakeLists.txt | 3 + MRI2CBCT/CMakeLists.txt | 31 ++ MRI2CBCT/MRI2CBCT.py | 383 +++++++++++++++++++++++++ MRI2CBCT/Resources/Icons/MRI2CBCT.png | Bin 0 -> 21024 bytes MRI2CBCT/Resources/UI/MRI2CBCT.ui | 234 +++++++++++++++ MRI2CBCT/Testing/CMakeLists.txt | 1 + MRI2CBCT/Testing/Python/CMakeLists.txt | 2 + MRI2CBCT_CLI/CMakeLists.txt | 6 + MRI2CBCT_CLI/MRI2CBCT_CLI.py | 32 +++ MRI2CBCT_CLI/MRI2CBCT_CLI.xml | 37 +++ 10 files changed, 729 insertions(+) create mode 100644 MRI2CBCT/CMakeLists.txt create mode 100644 MRI2CBCT/MRI2CBCT.py create mode 100644 MRI2CBCT/Resources/Icons/MRI2CBCT.png create mode 100644 MRI2CBCT/Resources/UI/MRI2CBCT.ui create mode 100644 MRI2CBCT/Testing/CMakeLists.txt create mode 100644 MRI2CBCT/Testing/Python/CMakeLists.txt create mode 100644 MRI2CBCT_CLI/CMakeLists.txt create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 39bea8c..4098c47 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,9 @@ add_subdirectory(AREG) add_subdirectory(AutoMatrix) add_subdirectory(AutoCrop3D) +add_subdirectory(MRI2CBCT) +add_subdirectory(MRI2CBCT) +add_subdirectory(MRI2CBCT_CLI) ## NEXT_MODULE #----------------------------------------------------------------------------- diff --git a/MRI2CBCT/CMakeLists.txt b/MRI2CBCT/CMakeLists.txt new file mode 100644 index 0000000..f47e2ac --- /dev/null +++ b/MRI2CBCT/CMakeLists.txt @@ -0,0 +1,31 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME MRI2CBCT) + +#----------------------------------------------------------------------------- +set(MODULE_PYTHON_SCRIPTS + ${MODULE_NAME}.py + ) + +set(MODULE_PYTHON_RESOURCES + Resources/Icons/${MODULE_NAME}.png + Resources/UI/${MODULE_NAME}.ui + ) + +#----------------------------------------------------------------------------- +slicerMacroBuildScriptedModule( + NAME ${MODULE_NAME} + SCRIPTS ${MODULE_PYTHON_SCRIPTS} + RESOURCES ${MODULE_PYTHON_RESOURCES} + WITH_GENERIC_TESTS + ) + +#----------------------------------------------------------------------------- +if(BUILD_TESTING) + + # Register the unittest subclass in the main script as a ctest. + # Note that the test will also be available at runtime. + slicer_add_python_unittest(SCRIPT ${MODULE_NAME}.py) + + # Additional build-time testing + add_subdirectory(Testing) +endif() diff --git a/MRI2CBCT/MRI2CBCT.py b/MRI2CBCT/MRI2CBCT.py new file mode 100644 index 0000000..99b6911 --- /dev/null +++ b/MRI2CBCT/MRI2CBCT.py @@ -0,0 +1,383 @@ +import logging +import os +from typing import Annotated, Optional + +import vtk + +import slicer +from slicer.i18n import tr as _ +from slicer.i18n import translate +from slicer.ScriptedLoadableModule import * +from slicer.util import VTKObservationMixin +from slicer.parameterNodeWrapper import ( + parameterNodeWrapper, + WithinRange, +) + +from slicer import vtkMRMLScalarVolumeNode + + +# +# MRI2CBCT +# + + +class MRI2CBCT(ScriptedLoadableModule): + """Uses ScriptedLoadableModule base class, available at: + https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent): + ScriptedLoadableModule.__init__(self, parent) + self.parent.title = _("MRI2CBCT") # TODO: make this more human readable by adding spaces + # TODO: set categories (folders where the module shows up in the module selector) + self.parent.categories = [translate("qSlicerAbstractCoreModule", "Examples")] + self.parent.dependencies = [] # TODO: add here list of module names that this module requires + self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)" + # TODO: update with short description of the module and a link to online module documentation + # _() function marks text as translatable to other languages + self.parent.helpText = _(""" +This is an example of scripted loadable module bundled in an extension. +See more information in module documentation. +""") + # TODO: replace with organization, grant and thanks + self.parent.acknowledgementText = _(""" +This file was originally developed by Jean-Christophe Fillion-Robin, Kitware Inc., Andras Lasso, PerkLab, +and Steve Pieper, Isomics, Inc. and was partially funded by NIH grant 3P41RR013218-12S1. +""") + + # Additional initialization step after application startup is complete + slicer.app.connect("startupCompleted()", registerSampleData) + + +# +# Register sample data sets in Sample Data module +# + + +def registerSampleData(): + """Add data sets to Sample Data module.""" + # It is always recommended to provide sample data for users to make it easy to try the module, + # but if no sample data is available then this method (and associated startupCompeted signal connection) can be removed. + + import SampleData + + iconsPath = os.path.join(os.path.dirname(__file__), "Resources/Icons") + + # To ensure that the source code repository remains small (can be downloaded and installed quickly) + # it is recommended to store data sets that are larger than a few MB in a Github release. + + # MRI2CBCT1 + SampleData.SampleDataLogic.registerCustomSampleDataSource( + # Category and sample name displayed in Sample Data module + category="MRI2CBCT", + sampleName="MRI2CBCT1", + # Thumbnail should have size of approximately 260x280 pixels and stored in Resources/Icons folder. + # It can be created by Screen Capture module, "Capture all views" option enabled, "Number of images" set to "Single". + thumbnailFileName=os.path.join(iconsPath, "MRI2CBCT1.png"), + # Download URL and target file name + uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95", + fileNames="MRI2CBCT1.nrrd", + # Checksum to ensure file integrity. Can be computed by this command: + # import hashlib; print(hashlib.sha256(open(filename, "rb").read()).hexdigest()) + checksums="SHA256:998cb522173839c78657f4bc0ea907cea09fd04e44601f17c82ea27927937b95", + # This node name will be used when the data set is loaded + nodeNames="MRI2CBCT1", + ) + + # MRI2CBCT2 + SampleData.SampleDataLogic.registerCustomSampleDataSource( + # Category and sample name displayed in Sample Data module + category="MRI2CBCT", + sampleName="MRI2CBCT2", + thumbnailFileName=os.path.join(iconsPath, "MRI2CBCT2.png"), + # Download URL and target file name + uris="https://github.com/Slicer/SlicerTestingData/releases/download/SHA256/1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97", + fileNames="MRI2CBCT2.nrrd", + checksums="SHA256:1a64f3f422eb3d1c9b093d1a18da354b13bcf307907c66317e2463ee530b7a97", + # This node name will be used when the data set is loaded + nodeNames="MRI2CBCT2", + ) + + +# +# MRI2CBCTParameterNode +# + + +@parameterNodeWrapper +class MRI2CBCTParameterNode: + """ + The parameters needed by module. + + inputVolume - The volume to threshold. + imageThreshold - The value at which to threshold the input volume. + invertThreshold - If true, will invert the threshold. + thresholdedVolume - The output volume that will contain the thresholded volume. + invertedVolume - The output volume that will contain the inverted thresholded volume. + """ + + inputVolume: vtkMRMLScalarVolumeNode + imageThreshold: Annotated[float, WithinRange(-100, 500)] = 100 + invertThreshold: bool = False + thresholdedVolume: vtkMRMLScalarVolumeNode + invertedVolume: vtkMRMLScalarVolumeNode + + +# +# MRI2CBCTWidget +# + + +class MRI2CBCTWidget(ScriptedLoadableModuleWidget, VTKObservationMixin): + """Uses ScriptedLoadableModuleWidget base class, available at: + https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self, parent=None) -> None: + """Called when the user opens the module the first time and the widget is initialized.""" + ScriptedLoadableModuleWidget.__init__(self, parent) + VTKObservationMixin.__init__(self) # needed for parameter node observation + self.logic = None + self._parameterNode = None + self._parameterNodeGuiTag = None + + def setup(self) -> None: + """Called when the user opens the module the first time and the widget is initialized.""" + ScriptedLoadableModuleWidget.setup(self) + + # Load widget from .ui file (created by Qt Designer). + # Additional widgets can be instantiated manually and added to self.layout. + uiWidget = slicer.util.loadUI(self.resourcePath("UI/MRI2CBCT.ui")) + self.layout.addWidget(uiWidget) + self.ui = slicer.util.childWidgetVariables(uiWidget) + + # Set scene in MRML widgets. Make sure that in Qt designer the top-level qMRMLWidget's + # "mrmlSceneChanged(vtkMRMLScene*)" signal in is connected to each MRML widget's. + # "setMRMLScene(vtkMRMLScene*)" slot. + uiWidget.setMRMLScene(slicer.mrmlScene) + + # Create logic class. Logic implements all computations that should be possible to run + # in batch mode, without a graphical user interface. + self.logic = MRI2CBCTLogic() + + # Connections + + # 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) + + # Make sure parameter node is initialized (needed for module reload) + self.initializeParameterNode() + + def cleanup(self) -> None: + """Called when the application closes and the module widget is destroyed.""" + self.removeObservers() + + def enter(self) -> None: + """Called each time the user opens this module.""" + # Make sure parameter node exists and observed + self.initializeParameterNode() + + def exit(self) -> None: + """Called each time the user opens a different module.""" + # Do not react to parameter node changes (GUI will be updated when the user enters into the module) + if self._parameterNode: + self._parameterNode.disconnectGui(self._parameterNodeGuiTag) + self._parameterNodeGuiTag = None + self.removeObserver(self._parameterNode, vtk.vtkCommand.ModifiedEvent, self._checkCanApply) + + 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) + + def onSceneEndClose(self, caller, event) -> None: + """Called just after the scene is closed.""" + # If this module is shown while the scene is closed then recreate a new parameter node immediately + if self.parent.isEntered: + self.initializeParameterNode() + + def initializeParameterNode(self) -> None: + """Ensure parameter node exists and observed.""" + # 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()) + + # 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 + + def setParameterNode(self, inputParameterNode: Optional[MRI2CBCTParameterNode]) -> None: + """ + Set and observe parameter node. + Observation is needed because when the parameter node is changed then the GUI must be updated immediately. + """ + + 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) + + +# +# MRI2CBCTLogic +# + + +class MRI2CBCTLogic(ScriptedLoadableModuleLogic): + """This class should implement all the actual + computation done by your module. The interface + should be such that other python code can import + this class and make use of the functionality without + requiring an instance of the Widget. + Uses ScriptedLoadableModuleLogic base class, available at: + https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def __init__(self) -> None: + """Called when the logic class is instantiated. Can be used for initializing member variables.""" + ScriptedLoadableModuleLogic.__init__(self) + + def getParameterNode(self): + return MRI2CBCTParameterNode(super().getParameterNode()) + + def process(self, + inputVolume: vtkMRMLScalarVolumeNode, + outputVolume: vtkMRMLScalarVolumeNode, + imageThreshold: float, + invert: bool = False, + showResult: bool = True) -> None: + """ + Run the processing algorithm. + Can be used without GUI widget. + :param inputVolume: volume to be thresholded + :param outputVolume: thresholding result + :param imageThreshold: values above/below this threshold will be set to 0 + :param invert: if True then values above the threshold will be set to 0, otherwise values below are set to 0 + :param showResult: show output volume in slice viewers + """ + + if not inputVolume or not outputVolume: + raise ValueError("Input or output volume is invalid") + + import time + + startTime = time.time() + logging.info("Processing started") + + # Compute the thresholded output volume using the "Threshold Scalar Volume" CLI module + cliParams = { + "InputVolume": inputVolume.GetID(), + "OutputVolume": outputVolume.GetID(), + "ThresholdValue": imageThreshold, + "ThresholdType": "Above" if invert else "Below", + } + cliNode = slicer.cli.run(slicer.modules.thresholdscalarvolume, None, cliParams, wait_for_completion=True, update_display=showResult) + # We don't need the CLI module node anymore, remove it to not clutter the scene with it + slicer.mrmlScene.RemoveNode(cliNode) + + stopTime = time.time() + logging.info(f"Processing completed in {stopTime-startTime:.2f} seconds") + + +# +# MRI2CBCTTest +# + + +class MRI2CBCTTest(ScriptedLoadableModuleTest): + """ + This is the test case for your scripted module. + Uses ScriptedLoadableModuleTest base class, available at: + https://github.com/Slicer/Slicer/blob/main/Base/Python/slicer/ScriptedLoadableModule.py + """ + + def setUp(self): + """Do whatever is needed to reset the state - typically a scene clear will be enough.""" + slicer.mrmlScene.Clear() + + def runTest(self): + """Run as few or as many tests as needed here.""" + self.setUp() + self.test_MRI2CBCT1() + + def test_MRI2CBCT1(self): + """Ideally you should have several levels of tests. At the lowest level + tests should exercise the functionality of the logic with different inputs + (both valid and invalid). At higher levels your tests should emulate the + way the user would interact with your code and confirm that it still works + the way you intended. + One of the most important features of the tests is that it should alert other + developers when their changes will have an impact on the behavior of your + module. For example, if a developer removes a feature that you depend on, + your test should break so they know that the feature is needed. + """ + + self.delayDisplay("Starting the test") + + # Get/create input data + + import SampleData + + registerSampleData() + inputVolume = SampleData.downloadSample("MRI2CBCT1") + self.delayDisplay("Loaded test data set") + + inputScalarRange = inputVolume.GetImageData().GetScalarRange() + self.assertEqual(inputScalarRange[0], 0) + self.assertEqual(inputScalarRange[1], 695) + + outputVolume = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLScalarVolumeNode") + threshold = 100 + + # Test the module logic + + logic = MRI2CBCTLogic() + + # Test algorithm with non-inverted threshold + logic.process(inputVolume, outputVolume, threshold, True) + outputScalarRange = outputVolume.GetImageData().GetScalarRange() + self.assertEqual(outputScalarRange[0], inputScalarRange[0]) + self.assertEqual(outputScalarRange[1], threshold) + + # Test algorithm with inverted threshold + logic.process(inputVolume, outputVolume, threshold, False) + outputScalarRange = outputVolume.GetImageData().GetScalarRange() + self.assertEqual(outputScalarRange[0], inputScalarRange[0]) + self.assertEqual(outputScalarRange[1], inputScalarRange[1]) + + self.delayDisplay("Test passed") diff --git a/MRI2CBCT/Resources/Icons/MRI2CBCT.png b/MRI2CBCT/Resources/Icons/MRI2CBCT.png new file mode 100644 index 0000000000000000000000000000000000000000..5d83ab4f05067d6d5e30808fe07df6b4ac035349 GIT binary patch literal 21024 zcmV)tK$pLXP)Px#24YJ`L;(K){{a7>y{D4^000SaNLh0L01FcU01FcV0GgZ_00007bV*G`2iyW1 z2_`QyE^zn&03ZNKL_t(|+U&h~lpV!+2l}h3?t5p|j7IB>M$%{lEg<$q!U6+!z+g6G zRuf{J=jX&*oH*VcFG=h;PUP6Jv7J0`EM~_n7T6A8KuCy9MhIfT$5&Jq;{s+EoRjRQuigo>!uxJ;XTZ{#<@OeNbeSl22%0PKVBOAAl{vV zhcqQ^=g70|>p-q=hRCpOw}L5S<*#+JO}BKp}Kr$4EnTdWl*Z z97p2=vr9#ILbxeA}AsdV<1JK1m8oIR>~i%UQ8%ZbEr4H%!rxB!FD10=vW z2a^g)Do7$IVo*|ryPiRx016~kxN#T{e~a#6;U5twkm0|Kg>4h<^gX660i@Xq1SrpP}w=?fS z2;14$lmZ1Rl(xa30FaVkDx8n#nSlgJ=@6-|=}WI0=w9>C!J{sKrGIsg1BT0)a2D~L z%ecsaaRd2W(kdTAD6{iUSoQU~`SM{=i1=1_6WB^7%x&Gz}VsgEDAf z05cdxX7m-*N)JlKCz;U$5CB(bDIui?h_l!TX+Su(f+o+>_&!YV}XsDsX{H4Yy-7i^Vml(rQzN=AaRJHdhrJ%HE z8XrC!3>+Z1q|`gl{p6ia%?WVkM}9U=0Vgsc91;Qq2Z}K;aB!vp3ZqyxX;!z*YdM9Q z%7mgrX}-!c6e{{CQ)vZ=envtZ<4IL*jl#&1jR^65AZ>>jjRuzFh&-iSBGf8lY!xyr zOkz$&vc@PO4s+}HPO*;kvygf}ZdAcmUmSo;DVvzkZxZ4S;v6Jg5cfemfC8ox(E;wognT1dS%yGfo92nPV5)>XqMgd|gHxy}Oi3r(&=0l*-+BnwnsE<9@iP?tI z$91Be7Uzu0H!yv)PaI>hPTcU+NM~+z?V5iX9!E%O35BJhBMR>kPRMhmOd{ZP zUq(x4+9mkB#e1{bCFmBhp5E?D%G!3OFB^s1G%ybMzC|}5`&s6i(%0;zv5hQ$sj=g!zRXye=NTG#h!dEW+#*f` z2BoM$!JiQ=07a?;%xPxA0SOK={EG`Hk}cnM{qW%vr2MGG`g2CQbD$mnkn zvKk760~Jh-762)~jd`deE%* z8K4qq8DChKAVO6$A1&pac+V|8D|LLP1qu0nW361Qm6{MTR> zBl_7pE22SU(;!tX4aOsf00c&=&?2QdEhNQgJ8O)Lw^M{yEVQmHBUv;C`aNuqAoDJP zH3sJS(YxukQd;d0j#GqhC~{RJ6#^~W^iUk>YKJx6b=tVclc% zJ)UySiWY`-&CpHuwJ~T=lx!EhuO0(tp?9N%Nk7XTqi76_q`+cKlo&g*@Cmcpk<~8s zd}b-g((`1-iL^q9I8+d75)Fvd7(jvLSa{_70SJDms4xiI_hTJ7V_F8byl#9y23!?z`?d70t00pf>m>> z7_1S|AVbV@EHp%(N2Z;h;VmS?!8djSn|8)@KeoPKKP%BDh!r)7v9*+Aksd~^#mvm#W@6H;DTXd14DDp!?Xrq;~^K@Mt~p?mMb^YqNe3k3Waswx#ZKf z&EwR+S+P7q`Gy9>G?;dgs3k2Po{)C5Qp_lxY}Kvd*Ab3T@v{i}bhUWD5hIOJDDsqs zg&BgFWtzT>TG?O;P4~KQU!Hj9Q~u_O<>ZHc97_8o#I^u#@nmx}8sBw8eB$^~fCLV{ z^1z9`FWq|yN#9M_l!>5%YP|}o^?vd2#B%R;3~qe^lh1hH@+6<9_{b6*ZEgi;Oc+8l zS5Uq{AUTu}kX*nK;X=qe0yD=0lN+j-J>=ufBRT9Z)pnIR|$Xgiuh5LpUOIg(5;zT^?slXvE@K z4t5SOyt*&IjsdN=P@3-w)cDqg;Mlwi1Gp1|M2VCV$}$oeS9E19G_wOUyzo`z(xv;g z`Fvym8_j@aaD&y3k@b8QvoM3N7zYGpzHg*_1cNdlQ`*)*vSib|M!dHE*)e+Sn1}Vf z^5EI_rYo-ltUA?fli;P8DPuIr+G>QuCMbfBTG4vaA@loIb;>|@QYG9G`uA4f)f zy<43aSlOQ-Nc!NDuy{7N{Qi7Pe;{HxJW}##|Hsk7FpRb_@GaeeX(MAAVcA85HtyIu z6vxrhgADtM;u%=%e`&W1WGyXz5V8>C;-+;45eNjd&zzJXE@QsOwAdysz5}3oq`TAZ zf5v;i_(Xl{%n=jaQa7P%q;A5@Et5~bZcCwiecOT6KYb?|WsV`3g?vj;fbpULN0s0O z0uJX;EL;FWC=faDstR@W^_Vql7N&Q0LU0bLwPLR0V0@EVxPnYS9ve zlRx?R@^B)s))}kYhkjaO~qVO zl+>#*+tF2@2!{9Dl8j7Ym53hgH5>$h3_|*1Jpcn^mXl@lf|fmA%aexhD8HIdEcUu) zpNy=^c*4%zIOCKfb7$ZFD~fCbDq)K+?%O>6we=62^laarX~n`woA3KM#+YhsY8vY3 znAUy9S!ZuL?X)uv&X_(OT{C84-t6fKe!F?=&gH*)_`&J--*@jRyLRrFRwxu3h=`r^ z=_kJf0yg}6l0TpHSHE5!iBODT!@pjd{QYm9Tz+KzlQWOJ@#JaVU{8k%yqw5478=^; zZ*O1r;g@r*?PX)LPxoy;Z+`cRWS^gSe0d`9LjePuo}NCq`S*(|hxfLtYO#rw6x^*Z z=f`&(XqkV(#tElfxr1c|j)0*$&{Kc#x&J(;)U$c6tQMNMlk;-pr}eZge$VS|i!Rwy zvj8-!C$2TUdhDYGU<^Rf0uUB&fyHC=r1D&SNqMCv^ME~%e(mxp@3?JcbJrP%VDoxm zJ3g>jVZs=cR}tMS{%KiZ=gX(3{S%2uj0_J?d~0}k;#*rbpLWs3?^-@%=JYTQA@%vp zr$4oH#jhVq zmeYm=JU`}pS`Sb+dG5~AzV(aE1FHSIyY@c%)h?cED7fRM9CDkd^f$DhcwpRui}t$B z6RYqgp!|ft;I8{Vb9tq2+q`2Lz>ba%G&eQDH+<3hWK2E)Fjc8$gFs`)R}{Wm>XV-Q ziw0@;zjFVH>B(>J zoVn$^^OpW$&fIzZ!f{mZ!2`{kHoV!nb<38M+S=N{IEPZ%Joog|&)EF&Pk!p#WpZO1M61IJ-qgzcasWS0FdRO z@rPcz`=s_|f4*+dU|6MJ`o_wdF zv2emw@BjVu^Do|;YijcMzx48?P51u%!pf1pX#n6I+1u6s^8F`FJmcD}j9eUAbN5NI zJTyL9<-+wRoN?__<4?M52b98*H=me$`1MEL1x>59>SGZzkRWAa0DZg=Sfn#f?j;tB z8~S+9PncDharHOuIq>ZFFRC8eIcM~hoPQuKLGc)=il+0supAJ%6%& zTjPvV*PnIMr=D8a+zYOK4?CyN7@RzL>flYc+`1v3uY=M~mFdsIg^RZT^1cV3G~Z7? z<8_q>=Ia_(hkjoV@e{8{vea*3_l%-wZX9?_c}G#nJBUTYp05 z$<8{SV-POK>**aoF_-Cl-}Cb7h4y`mZ~KcUW-PmCpSfqovh#aeyJoKV{bxSK^M(#iR+W*KtfRAc z^-s^6aos=v7M#N$J~+kxeZtZ|eRW5*4Rt)gg2uxkD5c~YWTVKqx7_+?YvcPE_6-F% zRP;jk^$aFYPPii(X^Us-GyTF#wvC?!ICVg-j)U_&CU#uD?Kyh^4D?TI81IfAT*qP+ zy?w28s4wx}f4F`54S%?O`IxLAC?1;1xQD<~@C%0~+P}3dzHB?${sNq#<&?{|)+_+M zaY?I%LAD}DDo`~5F$r??3E-z=+f>Leqga1!?C=!orY#z*>sYiFO05MEyh3mN@Rr|q z4Q=?{xw16S77f~=on0gq;7d^8C8UVlxaq@w0BoyFz=T{GQ*uSUNeSgr8KqJgEiEnC zag67`voAW`iR1b8y%?!To{P1D-T z2`=J$d9s{)vru<5`Bp#DOG;12q(Jkl4YSYe#9OPjg91Ni|K>JKE>YD_%#%|;sa!)( z#ScU1phMHN7|4yB8OEu}*w6eLI)&8y0yL}u55~V<3U!^Go>0l6!K%rPfUtb?c5rk8#=A1BbqDKI@5ZKdu z1mi>r7td6|XFZK1-Cp4?Bp`Wr> z0VVzZLmNLJa{20U=W!uo&CQKaegGyM&;H39?1{#dG<)-o?61$#OHiPULr78&5c}v` z%QZ@5kUG}%y?gHd%j;TCz51n=`4@F3!lf)*KF`%9P%?A!71O6XZc48=5QCv3Ywtg6 z+7(}aN^rnofFcFXI2_l3SM{-d#|})KG!gAnr)G|k5CYXo1+vhOGpG7+90!cCOt#@L zAQ)r&?tbdYc_;tbN7f~-J^h=x_V@aU6Z^>y`%2Tp1%%^*^N`&L;lL3B%~RU@2KVlY z-uvqB{qz^p&w9sUqhmHfA?eS79ftrgh=%d~!AM`Egd5uQU-Kp{z3q)ip#)%f%Tu+I z6=QK6fpt9bNJL;9ii_}Hu<-vvUju^@<~xUC0^UIPjJ{{TfBnuoKJ>u@zx&rEgIj*z zSsgi4FBvhvT5yN9KHJ%|;vcU}9P{I+?N>s89FFD_FL=}bt+elr#Rnh%{N=5^E89)&u{M}DIdA@zmq?1qI<~lC&b#-WHXaaYWCtCM^ zJvpznxrI-dFd363wPRx26tuKVz}$Dfd$WDtZ@>DtSFgSMKj#hh?5hiWkCg_GxIJ&Y zHvaW{em3vP&wu=#Kf7YtZ80AgsGEA?R=fSs#^0RZzxMtGes#q4OT%vetM@G!-1yt` zYuJdMOVV}@C2Tza5tzO}fgh%PadhB#y$1e2==8*1_}s;!_(MT0_xP zaDj2{9ft<@?v8fmt3Ubvm9PHf`%#;Zt=hO80M5Maqnoxp^P7{0_IFPMfXd)N>x(~J ze&vflTz(WTAVH)sZt)cxN_$^FT~!OM;nD8r2421IvVm9cJBlJogcu+Q!Z?paY@msK z*Fhi@*jPYc^Mg3P-Io9t{;{$!&PA~7J=gvAqwl|<=f;y6u3rSae<8zn|Mgo>oN?w^ zYe$DtmYCSk2q6Rx^z>vRZgX@h!*liVFPvH)-`+k2r_Y^@Yfqew8)i(d`Mv9cca54> z#cgOTUGnwik4>05&9BSXA@AnFImd#_u73Sk#zMF*UHQGAJvw2|3A>LE2ZZy(a}EB4 z^FI6-bL&c@j@8^{YqCUN3e5b5c?puy!L|!Dw_ToXlPX2JfSG^#iyszttn92D*wpDA z>7F9XgDvEj8%YK(7*oQ{Ra;tGkIb7hd+%jeT)E+d6HXj)9K(}C{wwP04uceQ(kLZgy_BV5n%sVP(uoUt=3RW*t~9u;>sZv^am7E~ zapcvz&Z+ioo1?s96BDlIwsiC~FZh$!$DMrHo?Y8tSdzJlSTYe81AvZezx_#;tMBBw zCeGbDFeeWPH;w{QD!DMCJa;@cQ$|h=p{DTiDczXXP(?mhhkRW={LAN%b8`?*%ze$y zHGG`32A^wo!x#sVg75oaoMGyeshB!-N;L7wlg4_nC=x~`PKcDf0RXUI-n@hs&m7nP zn?60;d-3v1*o7}Iq=YDzO2o` zp}!mn3Ci&}w#^t!gzXPtdC4jrq?yQQ8QR%%R7TbF;d?$j--GXa@Vz|TTpo3GE*$P^ zSRufL2yOqILkJF62smyIj^n^_UAT^b>$u3**CXdTkbw;K`57mH(H+D9Q5}WDF#Nyc z6Pk#L0zgP@(c`h1p0VtCVzKWdNvSn~Jjnu-RA3|lE`eO=8yMNF1rKkNGMVsDh!6}) z0+>z|X$4XQ3Sq1|Daf)!5O}CotEkK8;Z^g<lrv*3&itSpM&pAO2`<^ik`1J_0G>`yO1UijHaR zm@{V%nwp!mmx|JU35XOZN-0Pggsc6JIkCsH>$0w~v2J5W?}wq*NR@TXYGe4cDx{Q&2_H*=^WPiYQF>&+`mNiS|HmOOQv>%u^-)qG@FfB$3^J(z03ZNK zL_t(f!ms)We1*VQ@G3sa1rJAh3t(=ej-hY?NufzsSP3S5X=O@5(p`-+kT&I$2`h_E z06V^o{LnSpAp9ut5sNc#7^li8L4+SWFFGRTOUTy6IBu+|V+kf|FB)6)OkJatx*C)) zxYYJF)&gK~w4VN`JUemi7{6OAgOq|_3BrjEB)m!hzY@T!_$U>9RLTL$Llu-pJa|%(-3C*h!Rh!5P#VTBBCG@mUMnKZs@+RbviZ(Bmz?e&Q+9eAQ9XdeK&5% zh{M0QHNhse3Ry;0%1~&GqVGzs;HDacx4~eii95!%0)jqx+`Tu#ghON;m(D2hvytX z9e8~{K3$=69?4YNdd6z;Sd@6uj5(_1wAVM+$B8?8SC^7PB{%uca<|;bIc64k&V2sZ-ULWck8c>l62Ze*ad>-3| zt9a|R-RSE+j7nL-Q!a?-!gOp*M7veHZRw3;1$%0*WD==_6=10v0*N$ql1~1#TErVCw<6(E}(Tmx}{5NxvwKab!}OC#2`m z=INBd=Pgz=X1n7+{po5T`r2*H660HD!O;mBW-EmeFTf-Qymq*N!v}`Icn-X&9-O&a zFM>EpqmrTRgdyHsc2Wo7guL92mv9^%0IJ73=O79~G2U5Gaw^>yXW=}^pjHn+p)(ID zKguYc&;t-zNrucIM9GVQzMo&6P?b*p;6uy9z)@>5DcR=jb8E`xv3~^KJlbc`2}xD( zM^|lJ4gx+h(Lw9DHb^Pq>j)sm7#ak}*==>W;k;S+#o+>ah60@=I=0C}$8UguzPoNn zj^C{hEKiiTF?h;egk*tF0{VV-O|p*^{K4#LK%P=2Z*d`1tEva1S4U?&r z%GmK%WVPcs5RMbt2ZVs@x^NxW3JRf`cB5ZAiu|o=@A>$YJjOLQ!w>vSr&05W23O$x zMeU)CZN@-w9gD9*-wGLI$ow7@`A|Us@~hT(^Vl6vDLp4#hwi24Kt{U#DCom(C%83F znl96qC8aqAG>~HyJboC;Z1`1HoSBjHO^}C?H%+1k-^Xi*TCk{azL^+Im^I{$MLDl=ivwbQ8^5O(AJa( zDFX96IC%%Th8%J^30EjMjzX?p;83dVgb6_1Ce(#L)m7X+>F*KE)MM(!IAw%fO}v}0#v8t zP&H3y3JFDB*2!hhQ#yI170e5vQ^V-FRf)I&9V5qtPC0Hge1^~0xWx#okfPQy!Qwei z7{^|Kz@|by4pi&FH$OUOX!}bGh6fI{dsVN2F`%ibxin+ujDwfH=km=T__Ggf|Mi9f z{^OZ_n9)$g#BmaJ`FiZ$xuflkH(sB8u%~BQu~?W;tyG$VAaGna=e3S+9hp0K-u|0! zxpn=87hT*}qY2cXJ$v@lf9o6HIOmOZ>*fv(4K>x()p=cAT|L*|c;o9g+;sDvqfPWn zYu2=_c>M9%8#ip2cJRP~2_qvTO}_6tZZ7AIYiSu#Ep7Wd&%0p#uW+tZ2%_jf4Y%Q3g1ASTY)-f+e(FN>y*GBauJFiJlrgx#Je2~>AH zH@&)Z)RlQl`+?ldB^&cyOLsu9$QjH5fB*zX65tS+Dw&wp}T)*yObHQ(3UoA2xGo%rJFRg2$$-3=?h{*T{$Euppru;#_p6F&K| zk6l+N6rvEeQmItGY15`TU;D~e=DhOCD{GFn0B*hc=4=|O)qH4UE^qA?GqwYUH4(w;R*j~()gEkV!vGlu)VGQaV>k3Q&jo_3fKh1$W4j2~nt ziX?(659JC^fAdm#c*ne2mR)L+{X6EB`*+N%Zhvl9|nzH(Iql~~uf;3!6++SB-< z2k*aN#jhWmmRex$-~RPqUu92z`t#(96*aC?oN>I359@h(^#upkzC7)y-aTtx7&Z75 zL8Yf`dOPBgb^QFoE1%BIeb@H-oI||( z*}1*1Jos*sVIZR#>79lR_bu$4|E`Tt42lRXt>b&=op9pj*>mRZn>1z0(Dcsf)l*MB zRdny&)As%EednC@Z@dx3%H4O*FHd>z6<6&y-*5l^_f9%;e*A;&swsdDG4S-SWhv%z#PY4cHI+k<5xXiR3!U>aJR@VP&eqxik4Tb!|D}SGxeg0n7 z(BcRCU!PQY?XC+|p?4YpsNn-$l^xG5h%o?X2*7l;Qi4OFt2M7FsDPwDfJhZ}Fp`9+ zXSeUw1$MLC{7as%JN@QOP!d%DnD@TT?%;6V+Z@N-mG=H&LC*b?jY>LDQsUE}`{Lau zDAeC;uNG`|YZ) zel~hex&Khd_-S2mb8hJSm@$5fuD)UAvQK<=V;K7cEcwV^ZS_KX=b zYJBOZ&zMmYNa?$I?4vw}hkW?2u510#Cq8_AX=M1AXize69Y<)bZZ;Tf?&vHLCH3Xw z+NUMykU`$$ZpupfL?#KO5I}02kLe@*#&`j!I5{y%f+PjTWOyoMtIw=t&fKy-A#@E> zgf^!L$8oE^??r)%-at?N+0$o~=1e5)84N%{06BcH{zo>O$mObzyyvty-KALh5qd8?yjFTdv;DG}Mtz{yn+RO^q^Si+LF06Q6!jv-Uo7!bsGSZz&L{ESkb z^VnxFP=uD&mP7rA4$Uy_{_A(YnRorox4hAs&!Kf9P+wn-r?BA}s=Gv$J@?)RaHm$R>!WpB!-roF$=P&*EXs+E|rJ@!Ci99Vs8e`E$085qD=TsZ@8oYZH7!1{~7{0q19w8LSvwgNFAAc72h z$nr2)wEzmL6?*P#mP1Hn9ulEbg2^J00b(QztB8Xrw7TjdNu=U*>k1t6PF%3rUH~ty zUU|ObI^Oy3ym0%pj!wL=^0_%r|5q}Qaq%gqZeiLOM3i)sojzkmwY9Y+7#SXNURe43 zP75Pw|?+L+g;Zs#u)PX5Y%3B>3jO8 zOqp`m@_+f~C0jOc>M9nC^>uZ1)y~f8z3+Sfb?a{Z;D>fBI&sc9W0CTXWy|^>dgPHi z{^^_FJZJs7b#sP?hnw>Gyw}y$)pNs5H@$wtO*ik!(gsFs=j`+Pul(N6?|SLS|F-1t z_AOmrsaWsi>#E~AI(z3|e$Bd5uD^B1kCvTu&hZS-yTA4?FLs=I*8WY8KXBU6zP%km zwUWyLJM*SGIf@^<<;*gb&NPrQktp$eE=50+~VUGflDT4fAzxmXs$6at0CAQ&N$AXCp% zc&htg)dJuZ0a>ZgGhg|#6Nwap;vf*N;{fd9hNehilt?<%0}w=*bN~d!1$fR0_0#I@ z58D5HfJRDWOZXbn*uJM(bDQbsTHBA5glgG`SMlIiCHz2P-_FCRlmU<;_8X-*?nC=O5_oEUU={tym?zE?xgMNI_Kz${ z4j}Z(Gs>Nun^bc-BH1SACh!79L5X8X#Q&R40LDfS@Ev8x`<}%1H}_%Bwq6i%_=>~x zIeeesdn(Sl8ilPhfYpV56vo2B1yG+$c=3`z6u4>9_JDJcE5bTIdWTaw^$knSlc}BP z=)~A$v4Wf%xtTc$(aqO30XsLnT>;a?3Yfx$<|q+Xg-m@^@f5G5j1};|WepI8x${Ql z;~B2$Ue(8*ZGGt3c_@s}t8WIJ`nb+vx@dKjU{fFIo=PU*)`cqx4CL@|^8L`E)Ak;w zs4wj?6v(mX$@VLhmXBZp14zyLxt>6wtg`Be%bS>Mrk-Fl;ktlc$D^~W+&1A zu2W!pDL_Cp_6bF`3V+fGHUR}jCQ`@5OX!MWpty{@30BhMWE@N5YGq27Gjzj#Wf0en z5-cu05k&&Ac(S9&Ac_~9DV<|zFggWGzY}8sy9lEx03B7QFfnRop@?LiJI<0-EIG$( zhsWXc2?b24tA=jL^#AlO5AzfFQljYjC{{gGJP(`pA4K1@c?g(z8+d_P>%#cisd)13 zdO}3eSQ3-b#vtNx*G#HY?Kqo)hC7u!hUmZtrLhn-qAcEnWe>b05^pX^11LN_x#X{ z%T5#-iwfpQ31T1r$R2Iw0|+cTfe@)U^e~^03M3Bu49{0u@z!u7PH7#%{BcFpF&U?D zrDNN<1~HI^>AqALDg_uW2Pk+7{RJNf2P-%*P{ol_fWfN5kzo(zfj&%a+zP*1v_if#KvrS_D4hg~saO#N!*`^W zrUh09WPuzC*@nq5rjtq#Bn}h=4i1mQ=6V+=O(@~)W+0Hs>|IJbP?&$EA`F%!21`B$ z3Lg4LeDoIs92)V_KU6{AU=@dpKFX4#;Ds`EG$48r=Bs6l%Si~K6J8|3%2-^Db1cbZ zlxqw?Pzl94CX^1y^ueK!b0q2-B4t zE_fIjsfLq`Fpn{OKLk-+vo3v9gg}f(pj0t8F0-CK0T>HQ{M&Nwa&RsnTmdKVhH(OVS28BR91+%Lwg?k~VPbnD+9o&P@WBGMuIB13ymn>N{I*RT|$@&R=z+=3YND*VIa~s>fa5D`M$NkA^9kvE0l;@}d*gC}U zcTXP1-#xKv^gvEK)rwuon~?^gM)cIn-*MtYN|$jB9Ane4k8aD+Rms@D{&-FR0Qq`> zj_JIn)d(wNo**p*JBs^(#m2xyOI9u2m#a?A+v#}UzlE;=QHWcDyA;{?=>NU{~<0b>## zMlpyySu{E7D~NOtk!Mm6=*e|rerU3v2q(&l2H}KJQ0#4>QC(^;3$%n%kNKmxL@iPB zFH(|_E{7BUaSn8JWfaJ$42Q7-W()vbJW|DxS~iQ|vF;)AEwnt0QhESAFMZ<$yNEjyO-O_j4XaCVp5z@%p{$nKu@+wNMUv+ivt^lg$4tec@w~Y~(X!0YlxY!mc2BvEjhhm>D$gQKyb$$W9`kgOjCD?=a?O`fAw_iUbzS(``h7{ znm|&(sV}2x>H$n&x)Jj(-4XIMH`(s7Ta%~Eb@wjEmWNKl-c_fgJTL*m_0T$VH%`56 zC0e^i;Fkr~-Ek%ku3LoaNDG{Ng<89Imm4o$)iibTNSxR(p69jy{IyR5z$@SS5&+;Y zR&H3n>9Gf9ZhHK|(}r}G@LW@KVd_bzZa?o6pMQDWwCQELeU>ttF%BnR54WxkZhbul zcJ65Fd1dwNfn7VMmHPW8c*T(>6$FlOU9YaCb);?9y!|I#d-M8P=Uvoi5@sqH;K%2W z;zst|my*}tf6wx0)0hRhZ`E|{S-A+qsWESye1O_d--yn0cbKT{gVpZ)r4ZwqUg;GB`e zV~_dZ?s*v8JrCV4?xMvXcsK@uTJMZNCm51?(tTeW5j3w2WM;C0NVW`gBB^SX;e~Hp znMp|cGrV^eR{ql!(X%3qkam@^CO`4)awDGo`&$!}Zx3{}GH&0qzdb8P-@s@oNLUD)=-37CJ?)(FJZtn&H-&iwG>n0m?~y!xN#V*ksrAD;4aaO#U# z{K3c3erg}q{d5WTzjC(uxwyZ3Ho#v1x4m*|0Ty>$|J0j4_{>Sy+`NqmK^q>qckb)I z{7DqFSs5G{|LV_fKj|HR^@Vlet7ep?O=Uu9p4!nn5S`ygJU>|+P@P4J+CH$ z6>-`JpTLZF?2C9*=h?kzZeM|?J{JWmV(*%TSa{9WPz9Z_1?FDz0;VrH0021g%Jqo_ z0048|y$W6L=mBTI;+tNx7eG)dwnPh*jqawh@T%)qp8es!+GxI?^`Vb#_6j5UHy^q? zirL%u>We1;Sa(btl>r}k;J@y!k&bh2FmvezeNz@K{_QWWSQagS!M(dWj?H04tAvf; zCg;HZSH>Rm>1X$%dGZQ8{iTmscikFi3s+k7%jpT16do^}#|9w74r<;gCEurl74gas zZ@?=*Jle4|6}}&5DNBE5oxdl-8Z8~gn%}$Lu^Txzq#-Pv7$=ui(vA84=ou^*U46s0 zj771{UI2yO0~3#jJ(mt2%B{WQ$0zr^v9_ytsIM(3ml~AJ4lMNwg@&VcC{?I(7aBMl z{mt7MbKoo!(xcP81>vMKY1xl0ImRS2%)$V+9zed0V8x)?051R>mAwG!CzV25H~}J8 zy9yiIi;?Ub9>c5wxDiM0)|W3M>7?};*FL?J_FEV?wKEa4!zpS`000q)Nkl|+fAWICi zHYF2FAVvJ7}7D=t7uz^x0{gHoZ2 z*3k|kkxpJOfUHPW!<&u6;j}Y1V~a)1Yd^XgC%$(j+Ry4n!-OhS)xq$-CLG#25r^NJ zh9lc&;o6`6JmPU#Qh*GsJop(nB(je?7~8vJ^uW!HkKKQMuCb}Q=$e~$Fve)(WB1S5 zlnUDHJafsmW1g&3U}@e8EbSgGury^86S-Vfde-s2_l0?wcj*pDsh|*NcTJD^E4S-0 zKf4?CH0LZf_^eDu>}29|a%xu_qi9zWXAk+$2aDYD%cneOu}g9#6=AZuQtUAvUZo_Xh%pZ z(Q(Vv3NHTU!`Sm&7rI|uh>`v6@T_3PT;mAp+YY0(s|VAU?ur~0ol#8DQQ}05o`9L_ zqDdJv4i*~FTwjI|YE%~RuCIncn;Rd0;I!dX(B{%leD>wm>0PBHSYw&<5l9Io71|b_ z)&I_~-+tG+J8oYxw0mn;P%YPUCs)mlpVHg7=%RIU{=0Wz&F>TXjP-8}#@^W60T#Z0 zE8wiap65?P`N$-wAXfw9OxV}BDSFJG?LzmO+A(( zR8c8bF+5O)9}p@YpObs1g9xs)AqkAw8YE$<+~4p7D`zb)gLzCpYI- zJb~2CBgQ4)6yRV;`a9UthP?o|RmDYGrDP+FB(=_mq6DB$;>BycRgB%7YI%KBU;?Q? z58VdoB1T9SmB@S^5?(U3eWY=S1w742Q3l->uk?HehswDQd|H|F5VD17l3A8!gw*ch!SeYGa0{=p*vBS%NkoCdmKTj zai!GwBRK{P$$qWjj&lToCksYE2O*@3Mkw3Sz;Z#htA;Xe6*Cg?ab)V~LX4g%>vlA`3MTbWmG*0Ujkl9mv{345~0@) zNc-y>-fN<#4ZAVn+{QN|K{vO`y;%Ae#$x+N6qsXeN@3AXDa>Q$hNqHAnRzlaxB`%l zcF$uOaRE6I-eitiA;Mq+O8J|cn7|;24MkSb=&*he#m8LaWTGrTKqo%tgCGttNwm`y zFtgf8?HkCn-XSWml%m1V?FgO0+`lF-D6RKRj8l4SrB(eSR3K^W=L({$0(%U!bn0TtFqdx%C7!xz@A+c(wv6A zHFhR6sD=jHVV1}JNSVd}iiggE8YBqI5ESuLYfxykKSYj@iGqfpTHc?=0F>f&inM

gM5Oyb7$7#KJA2&j@3Tx-)UlJ%O+Q7&{q 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 f>+Cwaj(z + + Matrix_bis + + + + 0 + 0 + 462 + 725 + + + + + + + Inputs + + + + + + + + + + Search + + + + + + + + + + Search + + + + + + + Input CBCT file(s) : + + + + + + + Input MRI files(s): + + + + + + + + File + + + + + Folder + + + + + + + + + File + + + + + Folder + + + + + + + + + + + Mirror + + + + + + + + + + + + + + Output + + + + + + + + + + Search + + + + + + + Output folder : + + + + + + + + + + _apply + + + + + + + Suffix : + + + + + + + AutoFill + + + + + + + + + + + + + + false + + + Run the algorithm. + + + Apply matrix + + + + + + + + + + + true + + + 0 + + + + + + + + + + Number of processed files : + + + + + + + time : + + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + qMRMLWidget + QWidget +

qMRMLWidget.h
+ 1 + + + ctkCollapsibleButton + QWidget +
ctkCollapsibleButton.h
+ 1 +
+ + + + diff --git a/MRI2CBCT/Testing/CMakeLists.txt b/MRI2CBCT/Testing/CMakeLists.txt new file mode 100644 index 0000000..655007a --- /dev/null +++ b/MRI2CBCT/Testing/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(Python) diff --git a/MRI2CBCT/Testing/Python/CMakeLists.txt b/MRI2CBCT/Testing/Python/CMakeLists.txt new file mode 100644 index 0000000..5658d8b --- /dev/null +++ b/MRI2CBCT/Testing/Python/CMakeLists.txt @@ -0,0 +1,2 @@ + +#slicer_add_python_unittest(SCRIPT ${MODULE_NAME}ModuleTest.py) diff --git a/MRI2CBCT_CLI/CMakeLists.txt b/MRI2CBCT_CLI/CMakeLists.txt new file mode 100644 index 0000000..042bb5a --- /dev/null +++ b/MRI2CBCT_CLI/CMakeLists.txt @@ -0,0 +1,6 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME MRI2CBCT_CLI) + +SlicerMacroBuildScriptedCLI( + NAME ${MODULE_NAME} + ) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI.py b/MRI2CBCT_CLI/MRI2CBCT_CLI.py new file mode 100644 index 0000000..6719d86 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python-real + +import sys + + +def main(input, sigma, output): + import SimpleITK as sitk + + reader = sitk.ImageFileReader() + reader.SetFileName(input) + image = reader.Execute() + + pixelID = image.GetPixelID() + + gaussian = sitk.SmoothingRecursiveGaussianImageFilter() + gaussian.SetSigma(sigma) + image = gaussian.Execute(image) + + caster = sitk.CastImageFilter() + caster.SetOutputPixelType(pixelID) + image = caster.Execute(image) + + writer = sitk.ImageFileWriter() + writer.SetFileName(output) + writer.Execute(image) + + +if __name__ == "__main__": + if len(sys.argv) < 4: + print("Usage: MRI2CBCT_CLI ") + sys.exit(1) + main(sys.argv[1], float(sys.argv[2]), sys.argv[3]) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI.xml b/MRI2CBCT_CLI/MRI2CBCT_CLI.xml new file mode 100644 index 0000000..6f0d99f --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI.xml @@ -0,0 +1,37 @@ + + + Examples + 0 + MRI2CBCT_CLI + + 0.1.0. + https://github.com/username/project + + Andras Lasso (PerkLab) + + + + sigma + + 1 + + 1.0 + + + + + inputVolume + + input + 0 + + + + outputVolume + + output + 2 + + + + From cecd62bec0e46664179005f9e4f1bcf73855aa6c Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Fri, 5 Jul 2024 17:45:29 -0400 Subject: [PATCH 3/9] ENH : interface user close to be finish --- ASO_CBCT/LinearTransform_t.tfm | 5 + MRI2CBCT/MRI2CBCT.py | 378 +++++++++++++++++++++++--- MRI2CBCT/Resources/UI/MRI2CBCT.ui | 434 +++++++++++++++++++++++++----- 3 files changed, 715 insertions(+), 102 deletions(-) create mode 100644 ASO_CBCT/LinearTransform_t.tfm 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 - - - From 14227b287168df9e7977e02f14f602a8f7226fa9 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Fri, 12 Jul 2024 18:30:13 -0400 Subject: [PATCH 4/9] ENH : implementation of orient + center MRI. Start of resample --- CMakeLists.txt | 1 - MRI2CBCT/MRI2CBCT.py | 362 +++++++++++++++++- MRI2CBCT/Resources/UI/MRI2CBCT.ui | 40 +- MRI2CBCT/utils/Method.py | 240 ++++++++++++ MRI2CBCT/utils/Preprocess_CBCT.py | 302 +++++++++++++++ MRI2CBCT/utils/Preprocess_CBCT_MRI.py | 130 +++++++ MRI2CBCT/utils/Preprocess_MRI.py | 130 +++++++ MRI2CBCT/utils/utils_CBCT.py | 166 ++++++++ MRI2CBCT_CLI/CMakeLists.txt | 12 +- MRI2CBCT_CLI/ImportLibrary.cmake | 15 + MRI2CBCT_CLI/MRI2CBCT_CLI.py | 32 -- MRI2CBCT_CLI/MRI2CBCT_CLI.xml | 37 -- MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py | 6 + MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py | 214 +++++++++++ .../MRI2CBCT_CLI_utils/resample_create_csv.py | 48 +++ .../MRI2CBCT_ORIENT_CENTER_MRI/CMakeLists.txt | 7 + .../MRI2CBCT_ORIENT_CENTER_MRI.py | 106 +++++ .../MRI2CBCT_ORIENT_CENTER_MRI.xml | 40 ++ MRI2CBCT_CLI/MRI2CBCT_REG/CMakeLists.txt | 7 + MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py | 133 +++++++ MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml | 61 +++ .../MRI2CBCT_RESAMPLE_CBCT_MRI/CMakeLists.txt | 7 + .../MRI2CBCT_RESAMPLE_CBCT_MRI.py | 164 ++++++++ .../MRI2CBCT_RESAMPLE_CBCT_MRI.xml | 40 ++ 24 files changed, 2222 insertions(+), 78 deletions(-) create mode 100644 MRI2CBCT/utils/Method.py create mode 100644 MRI2CBCT/utils/Preprocess_CBCT.py create mode 100644 MRI2CBCT/utils/Preprocess_CBCT_MRI.py create mode 100644 MRI2CBCT/utils/Preprocess_MRI.py create mode 100644 MRI2CBCT/utils/utils_CBCT.py create mode 100644 MRI2CBCT_CLI/ImportLibrary.cmake delete mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI.py delete mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI.xml create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/CMakeLists.txt create mode 100644 MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.xml create mode 100644 MRI2CBCT_CLI/MRI2CBCT_REG/CMakeLists.txt create mode 100644 MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml create mode 100644 MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/CMakeLists.txt create mode 100644 MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 4098c47..c3d08db 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,7 +35,6 @@ add_subdirectory(AREG) add_subdirectory(AutoMatrix) add_subdirectory(AutoCrop3D) -add_subdirectory(MRI2CBCT) add_subdirectory(MRI2CBCT) add_subdirectory(MRI2CBCT_CLI) ## NEXT_MODULE diff --git a/MRI2CBCT/MRI2CBCT.py b/MRI2CBCT/MRI2CBCT.py index 0e96582..c370096 100644 --- a/MRI2CBCT/MRI2CBCT.py +++ b/MRI2CBCT/MRI2CBCT.py @@ -1,9 +1,17 @@ import logging import os from typing import Annotated, Optional -from qt import QApplication, QWidget, QTableWidget, QTableWidgetItem, QHeaderView,QSpinBox, QVBoxLayout, QLabel, QSizePolicy, QCheckBox, QFileDialog +from qt import QApplication, QWidget, QTableWidget, QTableWidgetItem, QHeaderView,QSpinBox, QVBoxLayout, QLabel, QSizePolicy, QCheckBox, QFileDialog,QMessageBox, QApplication, QProgressDialog +import qt +from utils.Preprocess_CBCT import Process_CBCT +from utils.Preprocess_MRI import Process_MRI +from utils.Preprocess_CBCT_MRI import Preprocess_CBCT_MRI +import time import vtk +import shutil +import urllib +import zipfile import slicer from functools import partial @@ -145,6 +153,8 @@ def __init__(self, parent=None) -> None: self.minus_checked_rows = set() self._parameterNode = None self._parameterNodeGuiTag = None + + def setup(self) -> None: """Called when the user opens the module the first time and the widget is initialized.""" @@ -164,6 +174,18 @@ def setup(self) -> None: # Create logic class. Logic implements all computations that should be possible to run # in batch mode, without a graphical user interface. self.logic = MRI2CBCTLogic() + + documentsLocation = qt.QStandardPaths.DocumentsLocation + self.documents = qt.QStandardPaths.writableLocation(documentsLocation) + self.SlicerDownloadPath = os.path.join( + self.documents, + slicer.app.applicationName + "Downloads", + "MRI2CBCT", + "MRI2CBCT_" + "CBCT", + ) + self.preprocess_cbct = Process_CBCT(self) + self.preprocess_mri = Process_MRI(self) + self.preprocess_mri_cbct = Preprocess_CBCT_MRI(self) # Connections # LineEditOutputReg @@ -182,6 +204,12 @@ def setup(self) -> None: 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")) + self.ui.pushButtonOrientCBCT.connect("clicked(bool)",self.orientCBCT) + self.ui.pushButtonResample.connect("clicked(bool)",self.resampleMRICBCT) + self.ui.pushButtonOrientMRI.connect("clicked(bool)",self.orientCenterMRI) + self.ui.pushButtonDownloadOrientCBCT.connect("clicked(bool)",partial(self.downloadModel,self.ui.lineEditOrientCBCT, "Orientation", True)) + self.ui.pushButtonDownloadSegCBCT.connect("clicked(bool)",partial(self.downloadModel,self.ui.lineEditSegCBCT, "Segmentation", True)) + # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() @@ -286,6 +314,19 @@ def setup(self) -> None: spinBox.setMaximum(10000) spinBox.setValue(443) self.tableWidgetResample.setCellWidget(0, 2, spinBox) + + def get_resample_values(self): + """ + Retrieves the resample values (X, Y, Z) from the QTableWidget. + + :param tableWidgetResample: QTableWidget instance containing the resample values. + :return: A tuple of three integers representing the resample values (X, Y, Z). + """ + x_value = self.tableWidgetResample.cellWidget(0, 0).value + y_value = self.tableWidgetResample.cellWidget(0, 1).value + z_value = self.tableWidgetResample.cellWidget(0, 2).value + + return [x_value, y_value, z_value] def onCheckboxOrientClicked(self, row, col, state): @@ -553,6 +594,325 @@ def onApplyButton(self) -> None: """Run processing when user clicks "Apply" button.""" print("get_normalization : ",self.getNormalization()) print("getCheckboxValuesOrient : ",self.getCheckboxValuesOrient()) + + + def downloadModel(self, lineEdit, name, test,_): + """Function to download the model files from the link in the getModelUrl function""" + + # To select the reference files (CBCT Orientation and Registration mode only) + listmodel = self.preprocess_cbct.getModelUrl() + print("listmodel : ",listmodel) + + urls = listmodel[name] + if isinstance(urls, str): + url = urls + _ = self.DownloadUnzip( + url=url, + directory=os.path.join(self.SlicerDownloadPath), + folder_name=os.path.join("Models", name), + num_downl=1, + total_downloads=1, + ) + model_folder = os.path.join(self.SlicerDownloadPath, "Models", name) + + elif isinstance(urls, dict): + for i, (name_bis, url) in enumerate(urls.items()): + _ = self.DownloadUnzip( + url=url, + directory=os.path.join(self.SlicerDownloadPath), + folder_name=os.path.join("Models", name, name_bis), + num_downl=i + 1, + total_downloads=len(urls), + ) + model_folder = os.path.join(self.SlicerDownloadPath, "Models", name) + + if not model_folder == "": + error = self.preprocess_cbct.TestModel(model_folder, lineEdit.name) + + if isinstance(error, str): + QMessageBox.warning(self.parent, "Warning", error) + + else: + lineEdit.setText(model_folder) + + def DownloadUnzip( + self, url, directory, folder_name=None, num_downl=1, total_downloads=1 + ): + out_path = os.path.join(directory, folder_name) + + if not os.path.exists(out_path): + # print("Downloading {}...".format(folder_name.split(os.sep)[0])) + os.makedirs(out_path) + + temp_path = os.path.join(directory, "temp.zip") + + # Download the zip file from the url + with urllib.request.urlopen(url) as response, open( + temp_path, "wb" + ) as out_file: + # Pop up a progress bar with a QProgressDialog + progress = QProgressDialog( + "Downloading {} (File {}/{})".format( + folder_name.split(os.sep)[0], num_downl, total_downloads + ), + "Cancel", + 0, + 100, + self.parent, + ) + progress.setCancelButton(None) + progress.setWindowModality(qt.Qt.WindowModal) + progress.setWindowTitle( + "Downloading {}...".format(folder_name.split(os.sep)[0]) + ) + # progress.setWindowFlags(qt.Qt.WindowStaysOnTopHint) + progress.show() + length = response.info().get("Content-Length") + if length: + length = int(length) + blocksize = max(4096, length // 100) + read = 0 + while True: + buffer = response.read(blocksize) + if not buffer: + break + read += len(buffer) + out_file.write(buffer) + progress.setValue(read * 100.0 / length) + QApplication.processEvents() + shutil.copyfileobj(response, out_file) + + # Unzip the file + with zipfile.ZipFile(temp_path, "r") as zip: + zip.extractall(out_path) + + # Delete the zip file + os.remove(temp_path) + + return out_path + + def orientCBCT(self)->None: + self.list_Processes_Parameters = self.preprocess_cbct.Process( + input_t1_folder=self.ui.LineEditCBCT.text, + folder_output=self.ui.lineEditOutputOrientCBCT.text, + model_folder_1=self.ui.lineEditSegCBCT.text, + add_in_namefile="oui", + merge_seg=False, + isDCMInput=False, + slicerDownload=self.SlicerDownloadPath, + ) + + self.onProcessStarted() + + # /!\ Launch of the first process /!\ + print("module name : ",self.list_Processes_Parameters[0]["Module"]) + print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) + + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], + ) + + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + + del self.list_Processes_Parameters[0] + + def orientCenterMRI(self): + self.list_Processes_Parameters = self.preprocess_mri.Process( + input_folder=self.ui.LineEditMRI.text, + direction=self.getCheckboxValuesOrient(), + output_folder=self.ui.lineEditOutputOrientMRI.text, + ) + + self.onProcessStarted() + + # /!\ Launch of the first process /!\ + print("module name : ",self.list_Processes_Parameters[0]["Module"]) + print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) + + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], + ) + + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + + del self.list_Processes_Parameters[0] + + def resampleMRICBCT(self): + print("self.ui.lineEditOutputOrientMRI.text : ",self.ui.lineEditOuputResample.text) + self.list_Processes_Parameters = self.preprocess_mri_cbct.Process( + input_folder=self.ui.LineEditMRI.text, + output_folder=self.ui.lineEditOuputResample.text, + resample_size=self.get_resample_values() + ) + + self.onProcessStarted() + + # /!\ Launch of the first process /!\ + print("module name : ",self.list_Processes_Parameters[0]["Module"]) + print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) + + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], + ) + + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + + del self.list_Processes_Parameters[0] + + + def onProcessStarted(self): + self.startTime = time.time() + + # self.ui.progressBar.setMaximum(self.nb_patient) + self.ui.progressBar.setValue(0) + self.ui.progressBar.setTextVisible(True) + self.ui.progressBar.setFormat("0%") + + self.ui.label_info.setText(f"Starting process") + + self.nb_extnesion_did = 0 + self.nb_extension_launch = len(self.list_Processes_Parameters) + + self.module_name_before = 0 + self.nb_change_bystep = 0 + + self.RunningUI(True) + + def onProcessUpdate(self, caller, event): + self.ui.progressBar.setVisible(False) + # timer = f"Time : {time.time()-self.startTime:.2f}s" + currentTime = time.time() - self.startTime + if currentTime < 60: + timer = f"Time : {int(currentTime)}s" + elif currentTime < 3600: + timer = f"Time : {int(currentTime/60)}min and {int(currentTime%60)}s" + else: + timer = f"Time : {int(currentTime/3600)}h, {int(currentTime%3600/60)}min and {int(currentTime%60)}s" + + self.ui.label_time.setText(timer) + # self.module_name = caller.GetModuleTitle() if self.module_name_bis is None else self.module_name_bis + self.ui.label_info.setText(f"Extension {self.module_name} is running. \n Number of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}") + # self.displayModule = self.displayModule_bis if self.displayModule_bis is not None else self.display[self.module_name.split(' ')[0]] + + if self.module_name_before != self.module_name: + print("Valeur progress barre : ",100*self.nb_extnesion_did/self.nb_extension_launch) + self.ui.progressBar.setValue(self.nb_extnesion_did/self.nb_extension_launch) + self.ui.progressBar.setFormat(f"{100*self.nb_extnesion_did/self.nb_extension_launch}%") + self.nb_extnesion_did += 1 + self.ui.label_info.setText( + f"Extension {self.module_name} is running. \n Number of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}" + ) + + + self.module_name_before = self.module_name + self.nb_change_bystep = 0 + + + if caller.GetStatus() & caller.Completed: + if caller.GetStatus() & caller.ErrorsMask: + # error + print("\n\n ========= PROCESSED ========= \n") + + print(self.process.GetOutputText()) + print("\n\n ========= ERROR ========= \n") + errorText = self.process.GetErrorText() + print("CLI execution failed: \n \n" + errorText) + # error + # errorText = caller.GetErrorText() + # print("\n"+ 70*"=" + "\n\n" + errorText) + # print(70*"=") + self.onCancel() + + else: + print("\n\n ========= PROCESSED ========= \n") + # print("PROGRESS :",self.displayModule.progress) + + print(self.process.GetOutputText()) + try: + print("name process : ",self.list_Processes_Parameters[0]["Process"]) + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], + ) + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + del self.list_Processes_Parameters[0] + # self.displayModule.progress = 0 + except IndexError: + self.OnEndProcess() + + def OnEndProcess(self): + self.nb_extnesion_did += 1 + self.ui.label_info.setText( + f"Process end" + ) + self.ui.progressBar.setValue(0) + + # if self.nb_change_bystep == 0: + # print(f'Erreur this module didnt work {self.module_name_before}') + + self.module_name_before = self.module_name + self.nb_change_bystep = 0 + total_time = time.time() - self.startTime + + + print("PROCESS DONE.") + print( + "Done in {} min and {} sec".format( + int(total_time / 60), int(total_time % 60) + ) + ) + + self.RunningUI(False) + + stopTime = time.time() + + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + + # setting message for Message Box + msg.setText(f"Processing completed in {stopTime-self.startTime:.2f} seconds") + + # setting Message box window title + msg.setWindowTitle("Information") + + # declaring buttons on Message Box + msg.setStandardButtons(QMessageBox.Ok) + msg.exec_() + + + def onCancel(self): + self.process.Cancel() + + self.RunningUI(False) + + def RunningUI(self, run=False): + + self.ui.progressBar.setVisible(run) + self.ui.label_time.setVisible(run) + self.ui.label_info.setVisible(run) + + + + # diff --git a/MRI2CBCT/Resources/UI/MRI2CBCT.ui b/MRI2CBCT/Resources/UI/MRI2CBCT.ui index 843e937..2de50c5 100644 --- a/MRI2CBCT/Resources/UI/MRI2CBCT.ui +++ b/MRI2CBCT/Resources/UI/MRI2CBCT.ui @@ -6,7 +6,7 @@ 0 0 - 535 + 735 1210 @@ -103,6 +103,13 @@ + + + + Download + + + @@ -110,6 +117,12 @@ + + + + + + @@ -117,8 +130,29 @@ - - + + + + + + + Orientation Model : + + + + + + + Segmentation Model : + + + + + + + Download + + diff --git a/MRI2CBCT/utils/Method.py b/MRI2CBCT/utils/Method.py new file mode 100644 index 0000000..4825b65 --- /dev/null +++ b/MRI2CBCT/utils/Method.py @@ -0,0 +1,240 @@ +from abc import ABC, abstractmethod +import os +import glob +import json + + +class Method(ABC): + def __init__(self, widget): + self.widget = widget + self.diccheckbox = {} + self.diccheckbox2 = {} + + @abstractmethod + def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): + """ + Count the number of patient in folder + Args: + scan_folder_t1 (str): folder path with Scan for T1 + scan_folder_t2 (str): folder path with Scan for T2 + + Return: + int : return the number of patient. + """ + pass + + @abstractmethod + def TestScan(self, scan_folder_t1: str, scan_folder_t2) -> str: + """Verify if the input folder seems good (have everything required to run the mode selected), if something is wrong the function return string with error message + + This function is called when the user want to import scan + + Args: + scan_folder (str): path of folder with scan + + Returns: + str or None: Return str with error message if something is wrong, else return None + pass + """ + + @abstractmethod + def TestReference(self, ref_folder: str) -> str: + """Verify if the reference folder contains reference gold files with landmarks and scans, if True return None and if False return str with error message to user + + Args: + ref_folder (str): folder path with gold landmark + + Return : + str or None : display str to user like warning + """ + + pass + + @abstractmethod + def TestModel(self, model_folder: str, lineEditName) -> str: + """Verify whether the model folder contains the right models used for ALI and other AI tool + + Args: + model_folder (str): folder path with different models + + Return : + str or None : display str to user like warning + """ + + pass + + @abstractmethod + def TestCheckbox(self) -> str: + pass + + @abstractmethod + def TestProcess(self, **kwargs) -> str: + """Check if everything is OK before launching the process, if something is wrong return string with all error + + + + Returns: + str or None: return None if there no problem with input of the process, else return str with all error + """ + pass + + @abstractmethod + def Process(self, **kwargs): + """Launch extension""" + + pass + + @abstractmethod + def DicLandmark(self): + """ + return dic landmark like this: + dic = {'teeth':{ + 'Lower':['LR6','LR5',...], + 'Upper':['UR6',...] + }, + 'Landmark':{ + 'Occlusual':['O',...], + 'Cervical':['R',...] + } + } + """ + + pass + + @abstractmethod + def existsLandmark(self, pathfile: str, pathref: str, pathmodel: str): + """return dictionnary. when the value of the landmark in dictionnary is true, the landmark is in input folder and in gold folder + Args: + pathfile (str): path + + Return : + dict : exemple dic = {'O':True,'UL6':False,'UR1':False,...} + """ + pass + + @abstractmethod + def getTestFileList(self): + """Return a tuple with both the name and the Download link of the test files + + tuple = ('name','link') + """ + pass + + @abstractmethod + def getReferenceList(self): + """ + Return a dictionnary with both the name and the Download link of the references + + dict = {'name1':'link1','name2':'link2',...} + + """ + pass + + @abstractmethod + def getModelUrl(self): + """ + Return dictionnary contains the url for each model + + dict = {'name':{'type1':'url1','type2':'url2'},...} + or + dict = {'name':'url'} + + """ + pass + + @abstractmethod + def getALIModelList(self): + """ + Return a tuple with both the name and the Download link for ALI model + else: + name, url = self.ActualMeth.getTestFileList() + + tuple = ('name','link') + + """ + pass + + def getcheckbox(self): + return self.diccheckbox + + def setcheckbox(self, dicccheckbox): + self.diccheckbox = dicccheckbox + + def getcheckbox2(self): + return self.diccheckbox2 + + def setcheckbox2(self, dicccheckbox): + self.diccheckbox2 = dicccheckbox + + def search(self, path, *args): + """ + Return a dictionary with args element as key and a list of file in path directory finishing by args extension for each key + + Example: + args = ('json',['.nii.gz','.nrrd']) + return: + { + 'json' : ['path/a.json', 'path/b.json','path/c.json'], + '.nii.gz' : ['path/a.nii.gz', 'path/b.nii.gz'] + '.nrrd.gz' : ['path/c.nrrd'] + } + """ + arguments = [] + for arg in args: + if type(arg) == list: + arguments.extend(arg) + else: + arguments.append(arg) + return { + key: [ + i + for i in glob.iglob( + os.path.normpath("/".join([path, "**", "*"])), recursive=True + ) + if i.endswith(key) + ] + for key in arguments + } + + + def ListLandmarksJson(self, json_file): + with open(json_file) as f: + data = json.load(f) + + return [ + data["markups"][0]["controlPoints"][i]["label"] + for i in range(len(data["markups"][0]["controlPoints"])) + ] + + def getTestFileListDCM(self): + """Return a tuple with both the name and the Download link of the test files but only for DCM files (AREG CBCT) + tuple = ('name','link') + """ + pass + + def TestScanDCM(self, scan_folder_t1: str, scan_folder_t2) -> str: + """Verify if the input folder seems good (have everything required to run the mode selected), if something is wrong the function return string with error message for DCM as input + + This function is called when the user want to import scan + + Args: + scan_folder (str): path of folder with scan + + Returns: + str or None: Return str with error message if something is wrong, else return None + """ + pass + + def NumberScanDCM(self, scan_folder_t1: str, scan_folder_t2: str): + """ + Count the number of patient in folder for DCM as input + Args: + scan_folder_t1 (str): folder path with Scan for T1 + scan_folder_t2 (str): folder path with Scan for T2 + + Return: + int : return the number of patient. + """ + pass + + \ No newline at end of file diff --git a/MRI2CBCT/utils/Preprocess_CBCT.py b/MRI2CBCT/utils/Preprocess_CBCT.py new file mode 100644 index 0000000..a7294c7 --- /dev/null +++ b/MRI2CBCT/utils/Preprocess_CBCT.py @@ -0,0 +1,302 @@ +from utils.Method import Method +from utils.utils_CBCT import GetDictPatients, GetPatients +import os, sys + +import SimpleITK as sitk +import numpy as np + +from glob import iglob +import slicer +import time +import qt +import platform + + +class Process_CBCT(Method): + def __init__(self, widget): + super().__init__(widget) + documentsLocation = qt.QStandardPaths.DocumentsLocation + documents = qt.QStandardPaths.writableLocation(documentsLocation) + self.tempAMASSS_folder = os.path.join( + documents, slicer.app.applicationName + "_temp_AMASSS" + ) + self.tempALI_folder = os.path.join( + documents, slicer.app.applicationName + "_temp_ALI" + ) + + def getGPUUsage(self): + if platform.system() == "Darwin": + return 1 + else: + return 5 + + def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): + return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) + + def getReferenceList(self): + return { + "Occlusal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Occlusal_Midsagittal_Plane.zip", + "Frankfurt Horizontal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Frankfurt_Horizontal_Midsagittal_Plane.zip", + } + + def TestReference(self, ref_folder: str): + out = None + scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] + lm_extension = [".json"] + + if self.NumberScan(ref_folder) == 0: + out = "The selected folder must contain scans" + + if self.NumberScan(ref_folder) > 1: + out = "The selected folder must contain only 1 case" + + return None + + def TestCheckbox(self, dic_checkbox): + list_landmark = self.CheckboxisChecked(dic_checkbox)[ + "Regions of Reference for Registration" + ] + + out = None + if len(list_landmark) == 0: + out = "Please select a Registration Type\n" + return out + + def TestModel(self, model_folder: str, lineEditName) -> str: + + if lineEditName == "lineEditSegCBCT": + if len(super().search(model_folder, "pth")["pth"]) == 0: + return "Folder must have models for mask segmentation" + else: + return None + + # if lineEditName == 'lineEditModelAli': + # if len(super().search(model_folder,'pth')['pth']) == 0: + # return 'Folder must have ALI models files' + # else: + # return None + + def TestProcess(self, **kwargs) -> str: + out = "" + + testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) + if testcheckbox is not None: + out += testcheckbox + + if kwargs["input_t1_folder"] == "": + out += "Please select an input folder for T1 scans\n" + + if kwargs["input_t2_folder"] == "": + out += "Please select an input folder for T2 scans\n" + + if kwargs["folder_output"] == "": + out += "Please select an output folder\n" + + if kwargs["add_in_namefile"] == "": + out += "Please select an extension for output files\n" + + if kwargs["model_folder_1"] == "": + out += "Please select a folder for segmentation models\n" + + if out == "": + out = None + + return out + + def getModelUrl(self): + return { + "Segmentation": { + "Full Face Models": "https://github.com/lucanchling/AMASSS_CBCT/releases/download/v1.0.2/AMASSS_Models.zip", + "Mask Models": "https://github.com/lucanchling/AMASSS_CBCT/releases/download/v1.0.2/Masks_Models.zip", + }, + "Orientation": { + "PreASO": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_preASOmodels/PreASOModels.zip", + "Occlusal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Occlusal_Midsagittal_Plane.zip", + "Frankfurt Horizontal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Frankfurt_Horizontal_Midsagittal_Plane.zip", + }, + } + + def getALIModelList(self): + return ( + "ALIModels", + "https://github.com/lucanchling/ALI_CBCT/releases/download/models_v01/", + ) + + def ProperPrint(self, notfound_list): + dic = { + "scanT1": "T1 scan", + "scanT2": "T2 scan", + "segT1": "T1 segmentation", + "segT2": "T2 segmentation", + } + out = "" + if "scanT1" in notfound_list and "scanT2" in notfound_list: + out += "T1 and T2 scans\n" + elif "segT1" in notfound_list and "segT2" in notfound_list: + out += "T1 and T2 segmentations\n" + else: + for notfound in notfound_list: + out += dic[notfound] + " " + return out + + def TestScan( + self, + scan_folder_t1: str, + scan_folder_t2: str, + liste_keys=["scanT1", "scanT2", "segT1"], + ): + out = "" + scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] + if self.NumberScan(scan_folder_t1, scan_folder_t2) == 0: + return "Please Select folder with scans" + + patients = GetDictPatients(scan_folder_t1, scan_folder_t2) + for patient, data in patients.items(): + not_found = [key for key in liste_keys if key not in data.keys()] + if len(not_found) != 0: + out += ( + f"Patient {patient} does not have {self.ProperPrint(not_found)}\n" + ) + + if out == "": # If no errors + out = None + + return out + + def GetSegmentationLabel(self, seg_folder): + seg_label = [] + patients = GetPatients(seg_folder) + seg_path = patients[list(patients.keys())[0]]["segT1"] + seg = sitk.ReadImage(seg_path) + seg_array = sitk.GetArrayFromImage(seg) + labels = np.unique(seg_array) + for label in labels: + if label != 0 and label not in seg_label: + seg_label.append(label) + return seg_label + + def CheckboxisChecked(self, diccheckbox: dict, in_str=False): + listchecked = {key: [] for key in diccheckbox.keys()} + for key, checkboxs in diccheckbox.items(): + for checkbox in checkboxs: + if checkbox.isChecked(): + listchecked[key] += [checkbox.text] + + return listchecked + + def DicLandmark(self): + return { + "Regions of Reference for Registration": [ + "Cranial Base", + "Mandible", + "Maxilla", + ], + "AMASSS Segmentation": [ + "Cranial Base", + "Cervical Vertebra", + "Mandible", + "Maxilla", + "Skin", + "Upper Airway", + ], + } + + def TranslateModels(self, listeModels, mask=False): + dicTranslate = { + "Models": { + "Mandible": "MAND", + "Maxilla": "MAX", + "Cranial Base": "CB", + "Cervical Vertebra": "CV", + "Root Canal": "RC", + "Mandibular Canal": "MCAN", + "Upper Airway": "UAW", + "Skin": "SKIN", + }, + "Masks": { + "Cranial Base": "CBMASK", + "Mandible": "MANDMASK", + "Maxilla": "MAXMASK", + }, + } + + translate = "" + for i, model in enumerate(listeModels): + if i < len(listeModels) - 1: + if mask: + translate += dicTranslate["Masks"][model] + " " + else: + translate += dicTranslate["Models"][model] + " " + else: + if mask: + translate += dicTranslate["Masks"][model] + else: + translate += dicTranslate["Models"][model] + + return translate + + def existsLandmark(self, input_dir, reference_dir, model_dir): + return None + + def getTestFileList(self): + return ( + "Semi-Automated", + "https://github.com/lucanchling/Areg_CBCT/releases/download/TestFiles/SemiAuto.zip", + ) + + def Process(self, **kwargs): + centered_T1 = kwargs["input_t1_folder"] + "_Center" + parameter_pre_aso = { + "input": kwargs["input_t1_folder"], + "output_folder": centered_T1, + "model_folder": os.path.join( + kwargs["slicerDownload"], "Models", "Orientation", "PreASO" + ), + "SmallFOV": False, + "temp_folder": "../", + "DCMInput": kwargs["isDCMInput"], + } + + PreOrientProcess = slicer.modules.pre_aso_cbct + list_process = [ + { + "Process": PreOrientProcess, + "Parameter": parameter_pre_aso, + "Module": "Centering CBCT", + # "Display": DisplayASOCBCT(nb_scan), + } + ] + + + + # AMASSS PROCESS - SEGMENTATION + AMASSSProcess = slicer.modules.amasss_cli + parameter_amasss_seg_t1 = { + "inputVolume": centered_T1, + "modelDirectory": kwargs["model_folder_1"], + "highDefinition": False, + "skullStructure": "CB", + "merge": "MERGE" if kwargs["merge_seg"] else "SEPARATE", + "genVtk": True, + "save_in_folder": True, + "output_folder": kwargs["folder_output"], + "precision": 50, + "vtk_smooth": 5, + "prediction_ID": "Pred", + "gpu_usage": self.getGPUUsage(), + "cpu_usage": 1, + "temp_fold": self.tempAMASSS_folder, + "SegmentInput": False, + "DCMInput": False, + } + + list_process.append( + { + "Process": AMASSSProcess, + "Parameter": parameter_amasss_seg_t1, + "Module": "AMASSS_CBCT Segmentation of CBCT", + # "Display": DisplayAMASSS(nb_scan, len(full_seg_struct)), + } + ) + + return list_process diff --git a/MRI2CBCT/utils/Preprocess_CBCT_MRI.py b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py new file mode 100644 index 0000000..8d4d508 --- /dev/null +++ b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py @@ -0,0 +1,130 @@ +from utils.Method import Method +from utils.utils_CBCT import GetDictPatients, GetPatients +import os, sys + +import SimpleITK as sitk +import numpy as np + +from glob import iglob +import slicer +import time +import qt +import platform + + +class Preprocess_CBCT_MRI(Method): + def __init__(self, widget): + super().__init__(widget) + documentsLocation = qt.QStandardPaths.DocumentsLocation + documents = qt.QStandardPaths.writableLocation(documentsLocation) + + + def getGPUUsage(self): + if platform.system() == "Darwin": + return 1 + else: + return 5 + + def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): + # return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) + return 0 + + def getReferenceList(self): + pass + + def TestReference(self, ref_folder: str): + out = None + scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] + lm_extension = [".json"] + + if self.NumberScan(ref_folder) == 0: + out = "The selected folder must contain scans" + + if self.NumberScan(ref_folder) > 1: + out = "The selected folder must contain only 1 case" + + return None + + def TestCheckbox(self, dic_checkbox): + pass + + def TestProcess(self, **kwargs) -> str: + out = "" + + testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) + if testcheckbox is not None: + out += testcheckbox + + if kwargs["input_folder"] == "": + out += "Please select an input folder for MRI scans\n" + + if kwargs["direction"] == "": + out += "Please select a direction for every axe\n" + + if kwargs["output_folder"] == "": + out += "Please select an output folder\n" + + if out == "": + out = None + + return out + + def getModelUrl(self): + pass + + def getALIModelList(self): + pass + + def TestModel(self, model_folder: str, lineEditName): + pass + + def ProperPrint(self, notfound_list): + pass + + def TestScan( + self, + scan_folder_t1: str, + scan_folder_t2: str, + liste_keys=["scanT1", "scanT2", "segT1"], + ): + pass + + def GetSegmentationLabel(self, seg_folder): + pass + + def CheckboxisChecked(self, diccheckbox: dict, in_str=False): + pass + + def DicLandmark(self): + pass + + def TranslateModels(self, listeModels, mask=False): + pass + + def existsLandmark(self, input_dir, reference_dir, model_dir): + return None + + def getTestFileList(self): + pass + + def Process(self, **kwargs): + list_process=[] + # MRI2CBCT_ORIENT_CENTER_MRI + MRI2CBCT_RESAMPLE_CBCT_MRI = slicer.modules.mri2cbct_resample_cbct_mri + parameter_mri2cbct_resample_cbct_mri = { + "input_folder": kwargs["input_folder"], + "output_folder": kwargs["output_folder"], + "resample_size": kwargs["resample_size"] + } + + list_process.append( + { + "Process": MRI2CBCT_RESAMPLE_CBCT_MRI, + "Parameter": parameter_mri2cbct_resample_cbct_mri, + "Module": "Resample files", + } + ) + + return list_process + + diff --git a/MRI2CBCT/utils/Preprocess_MRI.py b/MRI2CBCT/utils/Preprocess_MRI.py new file mode 100644 index 0000000..d8dc9a4 --- /dev/null +++ b/MRI2CBCT/utils/Preprocess_MRI.py @@ -0,0 +1,130 @@ +from utils.Method import Method +from utils.utils_CBCT import GetDictPatients, GetPatients +import os, sys + +import SimpleITK as sitk +import numpy as np + +from glob import iglob +import slicer +import time +import qt +import platform + + +class Process_MRI(Method): + def __init__(self, widget): + super().__init__(widget) + documentsLocation = qt.QStandardPaths.DocumentsLocation + documents = qt.QStandardPaths.writableLocation(documentsLocation) + + + def getGPUUsage(self): + if platform.system() == "Darwin": + return 1 + else: + return 5 + + def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): + # return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) + return 0 + + def getReferenceList(self): + pass + + def TestReference(self, ref_folder: str): + out = None + scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] + lm_extension = [".json"] + + if self.NumberScan(ref_folder) == 0: + out = "The selected folder must contain scans" + + if self.NumberScan(ref_folder) > 1: + out = "The selected folder must contain only 1 case" + + return None + + def TestCheckbox(self, dic_checkbox): + pass + + def TestProcess(self, **kwargs) -> str: + out = "" + + testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) + if testcheckbox is not None: + out += testcheckbox + + if kwargs["input_folder"] == "": + out += "Please select an input folder for MRI scans\n" + + if kwargs["direction"] == "": + out += "Please select a direction for every axe\n" + + if kwargs["output_folder"] == "": + out += "Please select an output folder\n" + + if out == "": + out = None + + return out + + def getModelUrl(self): + pass + + def getALIModelList(self): + pass + + def TestModel(self, model_folder: str, lineEditName): + pass + + def ProperPrint(self, notfound_list): + pass + + def TestScan( + self, + scan_folder_t1: str, + scan_folder_t2: str, + liste_keys=["scanT1", "scanT2", "segT1"], + ): + pass + + def GetSegmentationLabel(self, seg_folder): + pass + + def CheckboxisChecked(self, diccheckbox: dict, in_str=False): + pass + + def DicLandmark(self): + pass + + def TranslateModels(self, listeModels, mask=False): + pass + + def existsLandmark(self, input_dir, reference_dir, model_dir): + return None + + def getTestFileList(self): + pass + + def Process(self, **kwargs): + list_process=[] + # MRI2CBCT_ORIENT_CENTER_MRI + MRI2CBCT_ORIENT_CENTER_MRI = slicer.modules.mri2cbct_orient_center_mri + parameter_mri2cbct_orient_center_mri = { + "input_folder": kwargs["input_folder"], + "direction": kwargs["direction"], + "output_folder": kwargs["output_folder"] + } + + list_process.append( + { + "Process": MRI2CBCT_ORIENT_CENTER_MRI, + "Parameter": parameter_mri2cbct_orient_center_mri, + "Module": "Orientation and Centering of the MRI", + } + ) + + return list_process + + diff --git a/MRI2CBCT/utils/utils_CBCT.py b/MRI2CBCT/utils/utils_CBCT.py new file mode 100644 index 0000000..a4c247a --- /dev/null +++ b/MRI2CBCT/utils/utils_CBCT.py @@ -0,0 +1,166 @@ +import os +from glob import iglob + +def GetListNamesSegType(segmentationType): + dic = { + "CB": ["cb"], + "MAND": ["mand", "md"], + "MAX": ["max", "mx"], + } + return dic[segmentationType] + + +def GetListFiles(folder_path, file_extension): + """Return a list of files in folder_path finishing by file_extension""" + file_list = [] + for extension_type in file_extension: + file_list += search(folder_path, file_extension)[extension_type] + return file_list + + +def GetPatients(folder_path, time_point="T1", segmentationType=None): + """Return a dictionary with patient id as key""" + file_extension = [".nii.gz", ".nii", ".nrrd", ".nrrd.gz", ".gipl", ".gipl.gz"] + json_extension = [".json"] + file_list = GetListFiles(folder_path, file_extension + json_extension) + + patients = {} + + for file in file_list: + basename = os.path.basename(file) + patient = ( + basename.split("_Scan")[0] + .split("_scan")[0] + .split("_Or")[0] + .split("_OR")[0] + .split("_MAND")[0] + .split("_MD")[0] + .split("_MAX")[0] + .split("_MX")[0] + .split("_CB")[0] + .split("_lm")[0] + .split("_T2")[0] + .split("_T1")[0] + .split("_Cl")[0] + .split(".")[0] + ) + + if patient not in patients: + patients[patient] = {} + + if True in [i in basename for i in file_extension]: + # if segmentationType+'MASK' in basename: + if True in [i in basename.lower() for i in ["mask", "seg", "pred"]]: + if segmentationType is None: + patients[patient]["seg" + time_point] = file + else: + if True in [ + i in basename.lower() + for i in GetListNamesSegType(segmentationType) + ]: + patients[patient]["seg" + time_point] = file + + else: + patients[patient]["scan" + time_point] = file + + if True in [i in basename for i in json_extension]: + if time_point == "T2": + patients[patient]["lm" + time_point] = file + + return patients + + +def GetMatrixPatients(folder_path): + """Return a dictionary with patient id as key and matrix path as data""" + file_extension = [".tfm"] + file_list = GetListFiles(folder_path, file_extension) + + patients = {} + for file in file_list: + basename = os.path.basename(file) + patient = basename.split("reg_")[1].split("_Cl")[0] + if patient not in patients and True in [i in basename for i in file_extension]: + patients[patient] = {} + patients[patient]["mat"] = file + + return patients + + +def GetDictPatients( + folder_t1_path, + folder_t2_path, + segmentationType=None, + todo_str="", + matrix_folder=None, +): + """Return a dictionary with patients for both time points""" + patients_t1 = GetPatients( + folder_t1_path, time_point="T1", segmentationType=segmentationType + ) + patients_t2 = GetPatients(folder_t2_path, time_point="T2", segmentationType=None) + patients = MergeDicts(patients_t1, patients_t2) + + if matrix_folder is not None: + patient_matrix = GetMatrixPatients(matrix_folder) + patients = MergeDicts(patients, patient_matrix) + patients = ModifiedDictPatients(patients, todo_str) + return patients + + +def MergeDicts(dict1, dict2): + """Merge t1 and t2 dictionaries for each patient""" + patients = {} + for patient in dict1: + patients[patient] = dict1[patient] + try: + patients[patient].update(dict2[patient]) + except KeyError: + continue + return patients + + +def ModifiedDictPatients(patients, todo_str): + """Modify the dictionary of patients to only keep the ones in the todo_str""" + + if todo_str != "": + liste_todo = todo_str.split(",") + todo_patients = {} + for i in liste_todo: + patient = list(patients.keys())[int(i) - 1] + todo_patients[patient] = patients[patient] + patients = todo_patients + + return patients + + +def search(path, *args): + """ + Return a dictionary with args element as key and a list of file in path directory finishing by args extension for each key + + Example: + args = ('json',['.nii.gz','.nrrd']) + return: + { + 'json' : ['path/a.json', 'path/b.json','path/c.json'], + '.nii.gz' : ['path/a.nii.gz', 'path/b.nii.gz'] + '.nrrd.gz' : ['path/c.nrrd'] + } + """ + arguments = [] + for arg in args: + if type(arg) == list: + arguments.extend(arg) + else: + arguments.append(arg) + return { + key: sorted( + [ + i + for i in iglob( + os.path.normpath("/".join([path, "**", "*"])), recursive=True + ) + if i.endswith(key) + ] + ) + for key in arguments + } \ No newline at end of file diff --git a/MRI2CBCT_CLI/CMakeLists.txt b/MRI2CBCT_CLI/CMakeLists.txt index 042bb5a..d0acb86 100644 --- a/MRI2CBCT_CLI/CMakeLists.txt +++ b/MRI2CBCT_CLI/CMakeLists.txt @@ -1,6 +1,10 @@ #----------------------------------------------------------------------------- -set(MODULE_NAME MRI2CBCT_CLI) +add_subdirectory(MRI2CBCT_ORIENT_CENTER_MRI) +add_subdirectory(MRI2CBCT_RESAMPLE_CBCT_MRI) +# add_subdirectory(MRI2CBCT_CLI_utils) +# add_subdirectory(MRI2CBCT_RESAMPLE_CBCT_MRI) + +include(ImportLibrary.cmake) + + -SlicerMacroBuildScriptedCLI( - NAME ${MODULE_NAME} - ) diff --git a/MRI2CBCT_CLI/ImportLibrary.cmake b/MRI2CBCT_CLI/ImportLibrary.cmake new file mode 100644 index 0000000..9bdc896 --- /dev/null +++ b/MRI2CBCT_CLI/ImportLibrary.cmake @@ -0,0 +1,15 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME MRI2CBCT_CLI_utils) + +#----------------------------------------------------------------------------- +set(MODULE_PYTHON_SCRIPTS + ${MODULE_NAME}/__init__.py + ${MODULE_NAME}/resample_create_csv.py + ${MODULE_NAME}/resample.py +) + +#----------------------------------------------------------------------------- +slicerMacroBuildScriptedModule( + NAME ${MODULE_NAME} + SCRIPTS ${MODULE_PYTHON_SCRIPTS} +) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI.py b/MRI2CBCT_CLI/MRI2CBCT_CLI.py deleted file mode 100644 index 6719d86..0000000 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python-real - -import sys - - -def main(input, sigma, output): - import SimpleITK as sitk - - reader = sitk.ImageFileReader() - reader.SetFileName(input) - image = reader.Execute() - - pixelID = image.GetPixelID() - - gaussian = sitk.SmoothingRecursiveGaussianImageFilter() - gaussian.SetSigma(sigma) - image = gaussian.Execute(image) - - caster = sitk.CastImageFilter() - caster.SetOutputPixelType(pixelID) - image = caster.Execute(image) - - writer = sitk.ImageFileWriter() - writer.SetFileName(output) - writer.Execute(image) - - -if __name__ == "__main__": - if len(sys.argv) < 4: - print("Usage: MRI2CBCT_CLI ") - sys.exit(1) - main(sys.argv[1], float(sys.argv[2]), sys.argv[3]) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI.xml b/MRI2CBCT_CLI/MRI2CBCT_CLI.xml deleted file mode 100644 index 6f0d99f..0000000 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI.xml +++ /dev/null @@ -1,37 +0,0 @@ - - - Examples - 0 - MRI2CBCT_CLI - - 0.1.0. - https://github.com/username/project - - Andras Lasso (PerkLab) - - - - sigma - - 1 - - 1.0 - - - - - inputVolume - - input - 0 - - - - outputVolume - - output - 2 - - - - diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py new file mode 100644 index 0000000..4913410 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py @@ -0,0 +1,6 @@ +from .resample_create_csv import ( + create_csv +) + +# from .Net import DenseNet +from .resample import resample_images diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py new file mode 100644 index 0000000..64da3f9 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py @@ -0,0 +1,214 @@ +import SimpleITK as sitk +import numpy as np +import argparse +import os +import glob +import sys +import csv + +def resample_fn(img, args): + output_size = args.size + fit_spacing = args.fit_spacing + iso_spacing = args.iso_spacing + pixel_dimension = args.pixel_dimension + center = args.center + + if args.linear: + InterpolatorType = sitk.sitkLinear + else: + InterpolatorType = sitk.sitkNearestNeighbor + + + + spacing = img.GetSpacing() + size = img.GetSize() + + output_origin = img.GetOrigin() + output_size = [si if o_si == -1 else o_si for si, o_si in zip(size, output_size)] + + if(fit_spacing): + output_spacing = [sp*si/o_si for sp, si, o_si in zip(spacing, size, output_size)] + else: + output_spacing = spacing + + + if(iso_spacing): + output_spacing_filtered = [sp for si, sp in zip(args.size, output_spacing) if si != -1] + # print(output_spacing_filtered) + max_spacing = np.max(output_spacing_filtered) + output_spacing = [sp if si == -1 else max_spacing for si, sp in zip(args.size, output_spacing)] + # print(output_spacing) + + + if(args.spacing is not None): + output_spacing = args.spacing + + if(args.origin is not None): + output_origin = args.origin + + if(center): + output_physical_size = np.array(output_size)*np.array(output_spacing) + input_physical_size = np.array(size)*np.array(spacing) + output_origin = np.array(output_origin) - (output_physical_size - input_physical_size)/2.0 + + print("Input size:", size) + print("Input spacing:", spacing) + print("Output size:", output_size) + print("Output spacing:", output_spacing) + print("Output origin:", output_origin) + + resampleImageFilter = sitk.ResampleImageFilter() + resampleImageFilter.SetInterpolator(InterpolatorType) + resampleImageFilter.SetOutputSpacing(output_spacing) + resampleImageFilter.SetSize(output_size) + resampleImageFilter.SetOutputDirection(img.GetDirection()) + resampleImageFilter.SetOutputOrigin(output_origin) + # resampleImageFilter.SetDefaultPixelValue(zeroPixel) + + + return resampleImageFilter.Execute(img) + + +def Resample(img_filename, args): + + output_size = args.size + fit_spacing = args.fit_spacing + iso_spacing = args.iso_spacing + img_dimension = args.image_dimension + pixel_dimension = args.pixel_dimension + + print("Reading:", img_filename) + img = sitk.ReadImage(img_filename) + + if(args.img_spacing): + img.SetSpacing(args.img_spacing) + + return resample_fn(img, args) + + +def resample_images(args): + filenames = [] + if args.img: + fobj = {"img": args.img, "out": args.out} + filenames.append(fobj) + elif args.dir: + out_dir = args.out + normpath = os.path.normpath("/".join([args.dir, '**', '*'])) + for img in glob.iglob(normpath, recursive=True): + if os.path.isfile(img) and any(ext in img for ext in [".nrrd", ".nii", ".nii.gz", ".mhd", ".dcm", ".DCM", ".jpg", ".png"]): + fobj = {"img": img, "out": os.path.normpath(out_dir + "/" + img.replace(args.dir, ''))} + if args.out_ext is not None: + out_ext = args.out_ext if args.out_ext.startswith(".") else "." + args.out_ext + fobj["out"] = os.path.splitext(fobj["out"])[0] + out_ext + if not os.path.exists(os.path.dirname(fobj["out"])): + os.makedirs(os.path.dirname(fobj["out"])) + if not os.path.exists(fobj["out"]) or args.ow: + filenames.append(fobj) + elif args.csv: + replace_dir_name = args.csv_root_path + with open(args.csv) as csvfile: + csv_reader = csv.DictReader(csvfile) + for row in csv_reader: + fobj = {"img": row[args.csv_column], "out": row[args.csv_column]} + if replace_dir_name: + fobj["out"] = fobj["out"].replace(replace_dir_name, args.out) + if args.csv_use_spc: + img_spacing = [ + row[args.csv_column_spcx] if args.csv_column_spcx else None, + row[args.csv_column_spcy] if args.csv_column_spcy else None, + row[args.csv_column_spcz] if args.csv_column_spcz else None, + ] + fobj["img_spacing"] = [spc for spc in img_spacing if spc] + + if "ref" in row: + fobj["ref"] = row["ref"] + + if args.out_ext is not None: + out_ext = args.out_ext if args.out_ext.startswith(".") else "." + args.out_ext + fobj["out"] = os.path.splitext(fobj["out"])[0] + out_ext + if not os.path.exists(os.path.dirname(fobj["out"])): + os.makedirs(os.path.dirname(fobj["out"])) + if not os.path.exists(fobj["out"]) or args.ow: + filenames.append(fobj) + else: + raise ValueError("Set img or dir to resample!") + + if args.rgb: + if args.pixel_dimension == 3: + print("Using: RGB type pixel with unsigned char") + elif args.pixel_dimension == 4: + print("Using: RGBA type pixel with unsigned char") + else: + print("WARNING: Pixel size not supported!") + + if args.ref is not None: + print(args.ref) + ref = sitk.ReadImage(args.ref) + args.size = ref.GetSize() + args.spacing = ref.GetSpacing() + args.origin = ref.GetOrigin() + + for fobj in filenames: + try: + if "ref" in fobj and fobj["ref"] is not None: + ref = sitk.ReadImage(fobj["ref"]) + args.size = ref.GetSize() + args.spacing = ref.GetSpacing() + args.origin = ref.GetOrigin() + + if args.size is not None: + img = Resample(fobj["img"], args) + else: + img = sitk.ReadImage(fobj["img"]) + + print("Writing:", fobj["out"]) + writer = sitk.ImageFileWriter() + writer.SetFileName(fobj["out"]) + writer.UseCompressionOn() + writer.Execute(img) + + except Exception as e: + print(e, file=sys.stderr) + +def main(): + parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + in_group = parser.add_mutually_exclusive_group(required=True) + in_group.add_argument('--img', type=str, help='image to resample') + in_group.add_argument('--dir', type=str, help='Directory with image to resample') + in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') + + csv_group = parser.add_argument_group('CSV extra parameters') + csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') + csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') + csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') + csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') + + transform_group = parser.add_argument_group('Transform parameters') + transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) + transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) + transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') + transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') + transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') + transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) + transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) + transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) + transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) + + img_group = parser.add_argument_group('Image parameters') + img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) + img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) + img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) + + out_group = parser.add_argument_group('Output parameters') + out_group.add_argument('--ow', type=int, help='Overwrite', default=1) + out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") + out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) + + args = parser.parse_args() + resample_images(args) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py new file mode 100644 index 0000000..2c63fa8 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py @@ -0,0 +1,48 @@ +import SimpleITK as sitk +import os +import pandas as pd +import argparse + +def get_nifti_info(file_path,output_resample): + # Read the NIfTI file + image = sitk.ReadImage(file_path) + + # Get information + info = { + "in": file_path, + "out" : file_path.replace(os.path.dirname(file_path),output_resample), + "size": image.GetSize(), + "Spacing": image.GetSpacing(), + } + + return info + +def create_csv(input:str,output_resample:str,output_csv:str,name_csv:str): + if not os.path.exists(output_resample): + os.makedirs(output_resample) + + if not os.path.exists(output_csv): + os.makedirs(output_csv) + + input_folder = input + # Get all nifti files in the folder + nifti_files = [] + for root, dirs, files in os.walk(input_folder): + for file in files: + if file.endswith(".nii") or file.endswith(".nii.gz"): + nifti_files.append(os.path.join(root, file)) + + # Get nifti info for every nifti file + nifti_info = [] + for file in nifti_files: + info = get_nifti_info(file,output_resample) + nifti_info.append(info) + + # Créez un seul DataFrame avec toutes les informations + df = pd.DataFrame(nifti_info) + outpath = os.path.join(output_csv,name_csv) + df.to_csv(outpath, index=False) + + return outpath + + diff --git a/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/CMakeLists.txt b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/CMakeLists.txt new file mode 100644 index 0000000..d8762f7 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/CMakeLists.txt @@ -0,0 +1,7 @@ + +set(MODULE_NAME MRI2CBCT_ORIENT_CENTER_MRI) + +SlicerMacroBuildScriptedCLI( + NAME ${MODULE_NAME} +) + diff --git a/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py new file mode 100644 index 0000000..59c826f --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python-real + +import os +import SimpleITK as sitk +import argparse + +# THIS FILE IS WORKING WELL + +def extract_id(filename): + """ + Extracts and returns the ID from a filename, removing common NIfTI extensions. + + Parameters: + filename (str): The filename from which to extract the ID. + + Returns: + str: The extracted ID without the extension. + """ + # Remove the extension using os.path.splitext + type_file = 0 + base = os.path.splitext(filename)[0] + # If the file has a double extension (commonly .nii.gz), remove the second extension + if base.endswith('.nii'): + base = os.path.splitext(base)[0] + type_file=1 + + + return base,type_file + +def calculate_new_origin(image): + """ + Calculate the new origin to center the image in the Slicer viewport across all axes. + """ + size = image.GetSize() + spacing = image.GetSpacing() + # Calculate the center offset for each axis + new_origin = [(size[i] * spacing[i]) / 2 for i in range(len(size))] + new_origin = [new_origin[2],-new_origin[0],new_origin[1]] # FOR MRI + # new_origin = [-new_origin[0]*1.5,new_origin[1],-new_origin[2]*0.5] # FOR CBCT + # new_origin = [-new_origin[0]*1,new_origin[1],-new_origin[2]*1] # SAVE INSIDE BUT NOT CENTER + return tuple(new_origin) + +def modify_image_properties(nifti_file_path, new_direction, output_file_path=None): + """ + Read a NIfTI file, change its Direction and optionally center and save the modified image. + """ + image = sitk.ReadImage(nifti_file_path) + print("Original Direction:", image.GetDirection()) + print("Original Origin:", image.GetOrigin()) + + # Set the new direction + image.SetDirection(new_direction) + + # Calculate and set the new origin + new_origin = calculate_new_origin(image) + image.SetOrigin(new_origin) + + print("New Direction:", image.GetDirection()) + print("New Origin:", image.GetOrigin()) + + if output_file_path: + sitk.WriteImage(image, output_file_path) + print(f"Modified image saved to {output_file_path}") + + return image + +def main(args): + new_direction = tuple(map(float, args.direction.split(','))) # Assumes direction as comma-separated values + input_folder = args.input_folder + output_folder = args.output_folder if args.output_folder else input_folder # Default to input folder if no output folder is provided. + + # Ensure the output folder exists + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + # Get all nifti files in the folder + nifti_files = [] + for root, dirs, files in os.walk(input_folder): + for file in files: + if file.endswith(".nii") or file.endswith(".nii.gz"): + nifti_files.append(os.path.join(root, file)) + + # Process each file + for file_path in nifti_files: + filename = os.path.basename(file_path) + file_id,type_file = extract_id(filename) + if type_file==0: + output_file_path = os.path.join(output_folder, f"{file_id}_OR.nii") + else : + output_file_path = os.path.join(output_folder, f"{file_id}_OR.nii") + modify_image_properties(file_path, new_direction, output_file_path) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Modify NIfTI file directions and center them.") + parser.add_argument('input_folder', default = '.', help='Path to the input folder containing NIfTI files.') + parser.add_argument('direction', default = "-1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0", help='New direction for the NIfTI files, specified as a comma-separated string of floats. ') + parser.add_argument('output_folder', default = '.', help='Path to the output folder where modified NIfTI files will be saved.') + args = parser.parse_args() + main(args) + +# USE THIS DIRECTION FOR MRI : "0.0, 0.0, -1.0, 1.0, 0.0, 0.0, 0.0, -1.0, 0.0" +# FOR CBCT : "1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0" + + + +# "-1.0, 0.0, 0.0, 0.0, -1.0, 0.0, 0.0, 0.0, 1.0" \ No newline at end of file diff --git a/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.xml b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.xml new file mode 100644 index 0000000..04322f0 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.xml @@ -0,0 +1,40 @@ + + + Automated Dental Tools.Advanced + 2 + MRI2CBCT_ORIENT_CENTER_MRI + + 0.0.1 + https://github.com/username/project + Slicer + FirstName LastName (Institution), FirstName LastName (Institution) + This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN + + + + + + + + input_folder + + 0 + Path for the input folder. + + + + direction + + 1 + Direction for the new orientation of the scan + + + + output_folder + + 2 + Output Path + + + + diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/CMakeLists.txt b/MRI2CBCT_CLI/MRI2CBCT_REG/CMakeLists.txt new file mode 100644 index 0000000..aabe381 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/CMakeLists.txt @@ -0,0 +1,7 @@ +#----------------------------------------------------------------------------- +set(MODULE_NAME MRI2CBCT_REG) + + +SlicerMacroBuildScriptedCLI( + NAME ${MODULE_NAME} +) diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py new file mode 100644 index 0000000..02d9978 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python-real + +import argparse +import SimpleITK as sitk +import sys, os, time +import numpy as np + +fpath = os.path.join(os.path.dirname(__file__), "..") +sys.path.append(fpath) + +from ASO_CBCT_utils import ( + ExtractFilesFromFolder, + AngleAndAxisVectors, + RotationMatrix, + PreASOResample, + convertdicom2nifti, +) + + +def ResampleImage(image, transform): + """ + Resample image using SimpleITK + + Parameters + ---------- + image : SimpleITK.Image + Image to be resampled + target : SimpleITK.Image + Target image + transform : SimpleITK transform + Transform to be applied to the image. + + Returns + ------- + SimpleITK image + Resampled image. + """ + resample = sitk.ResampleImageFilter() + resample.SetReferenceImage(image) + resample.SetTransform(transform) + resample.SetInterpolator(sitk.sitkLinear) + orig_size = np.array(image.GetSize(), dtype=int) + ratio = 1 + new_size = orig_size * ratio + new_size = np.ceil(new_size).astype(int) # Image dimensions are in integers + new_size = [int(s) for s in new_size] + resample.SetSize(new_size) + resample.SetDefaultPixelValue(0) + + # Set New Origin + orig_origin = np.array(image.GetOrigin()) + # apply transform to the origin + orig_center = np.array( + image.TransformContinuousIndexToPhysicalPoint(np.array(image.GetSize()) / 2.0) + ) + # new_center = np.array(target.TransformContinuousIndexToPhysicalPoint(np.array(target.GetSize())/2.0)) + new_origin = orig_origin - orig_center + resample.SetOutputOrigin(new_origin) + + return resample.Execute(image) + + +def main(args): + + input_dir, out_dir, smallFOV, isDCMInput = ( + os.path.normpath(args.input[0]), + os.path.normpath(args.output_folder[0]), + args.SmallFOV[0] == "true", + args.DCMInput[0] == "true", + ) + + if isDCMInput: + convertdicom2nifti(input_dir) + + scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] + + if not os.path.exists(out_dir): + os.makedirs(out_dir) + + input_files, _ = ExtractFilesFromFolder(input_dir, scan_extension) + + for i in range(len(input_files)): + + input_file = input_files[i] + + img = sitk.ReadImage(input_file) + + # Translation to center volume + T = -np.array( + img.TransformContinuousIndexToPhysicalPoint(np.array(img.GetSize()) / 2.0) + ) + translation = sitk.TranslationTransform(3) + translation.SetOffset(T.tolist()) + + img_trans = ResampleImage(img, translation.GetInverse()) + img_out = img_trans + + # Write Scan + dir_scan = os.path.dirname(input_file.replace(input_dir, out_dir)) + if not os.path.exists(dir_scan): + os.makedirs(dir_scan) + + file_outpath = os.path.join(dir_scan, os.path.basename(input_file)) + if not os.path.exists(file_outpath): + sitk.WriteImage(img_out, file_outpath) + + print(f"""{0}""") + sys.stdout.flush() + time.sleep(0.2) + print(f"""{2}""") + sys.stdout.flush() + time.sleep(0.2) + print(f"""{0}""") + sys.stdout.flush() + time.sleep(0.2) + + +if __name__ == "__main__": + + print("PRE ASO") + + parser = argparse.ArgumentParser() + + parser.add_argument("input", nargs=1) + parser.add_argument("output_folder", nargs=1) + parser.add_argument("model_folder", nargs=1) + parser.add_argument("SmallFOV", nargs=1) + parser.add_argument("temp_folder", nargs=1) + parser.add_argument("DCMInput", nargs=1) + + args = parser.parse_args() + + main(args) diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml new file mode 100644 index 0000000..0b5d3a9 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml @@ -0,0 +1,61 @@ + + + Automated Dental Tools.Advanced + 1 + PRE_ASO_CBCT + + 0.0.1 + https://github.com/username/project + Slicer + FirstName LastName (Institution), FirstName LastName (Institution) + This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN + + + + + + + + input + + 0 + Path for the input. + + + + output_folder + + 1 + Output Path + + + + model_folder + + 2 + Folder with model files + + + + SmallFOV + + 4 + Boolean to say whether or not the input file is a Small FOV + + + + temp_folder + + 5 + Temp folder for pre aso resample for lightning models + + + + DCMInput + + 6 + Is Dicom as Input + + + + diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/CMakeLists.txt b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/CMakeLists.txt new file mode 100644 index 0000000..7495865 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/CMakeLists.txt @@ -0,0 +1,7 @@ +set(MODULE_NAME MRI2CBCT_RESAMPLE_CBCT_MRI) + + +SlicerMacroBuildScriptedCLI( + NAME ${MODULE_NAME} +) + diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py new file mode 100644 index 0000000..fdd2951 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python-real + +import subprocess +import csv +import argparse +import os +# from MRI2CBCT_CLI import MRI2CBCT_CLI_utils +# from MRI2CBCT_CLI_utils import ( +# create_csv, +# resample_images, + +# ) +import sys +fpath = os.path.join(os.path.dirname(__file__), "..") +sys.path.append(fpath) + +from MRI2CBCT_CLI_utils import create_csv, resample_images +import csv + +# THE PACING IS CHOOSEN RUNING CALCUL_SPACING_MEAN.PY DONC LA MOYENNE DES SPACING QUE ON A + +def run_resample(args): + # Remplacez ceci par le chemin vers votre fichier CSV + csv_file_path = args.csv + # Ouvrir le fichier CSV en lecture + #RESAMPLE + with open(csv_file_path, mode='r') as csv_file: + csv_reader = csv.DictReader(csv_file) + + # Boucle sur chaque ligne du fichier CSV + for row in csv_reader: + # Afficher les informations de chaque ligne + size = tuple(map(int, row["size"].strip("()").split(","))) + input_path = row["in"] + out_path = row["out"] + print(size[1]) + print(f'Image d\'entrée: {row["in"]}, Image de sortie: {row["out"]}, Taille: {size}') + # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 768 576 768 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 119 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 443 --fit_spacing True --center 0 --iso_spacing 1 --linear False --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + subprocess.run(command,shell=True) + +def create_args(img=None, dir=None, csv=None, csv_column='image', csv_root_path=None, csv_use_spc=0, + csv_column_spcx=None, csv_column_spcy=None, csv_column_spcz=None, ref=None, size=None, + img_spacing=None, spacing=None, origin=None, linear=False, center=0, fit_spacing=False, + iso_spacing=False, image_dimension=2, pixel_dimension=1, rgb=False, ow=1, out="./out.nrrd", + out_ext=None): + parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + in_group = parser.add_mutually_exclusive_group(required=True) + in_group.add_argument('--img', type=str, help='image to resample') + in_group.add_argument('--dir', type=str, help='Directory with image to resample') + in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') + + csv_group = parser.add_argument_group('CSV extra parameters') + csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') + csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') + csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') + csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') + + transform_group = parser.add_argument_group('Transform parameters') + transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) + transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) + transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') + transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') + transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') + transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) + transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) + transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) + transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) + + img_group = parser.add_argument_group('Image parameters') + img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) + img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) + img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) + + out_group = parser.add_argument_group('Output parameters') + out_group.add_argument('--ow', type=int, help='Overwrite', default=1) + out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") + out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) + + # Manually set the args + print(transform_size(size)) + args = parser.parse_args(args=[ + '--img', img, + '--size', "119 443 443", + '--linear', str(linear), + '--center', str(center), + '--fit_spacing', str(fit_spacing), + '--iso_spacing', str(iso_spacing), + '--image_dimension', str(image_dimension), + '--pixel_dimension', str(pixel_dimension), + '--rgb', str(rgb), + '--ow', str(ow), + '--out', out, + ]) + return args + +def transform_size(size_str): + """ + Transforms a string '[x,y,z]' into 'x y z' with x, y, z as integers. + + :param size_str: String in the format '[x,y,z]' + :return: String in the format 'x y z' + """ + # Remove the brackets and split by comma + size_list = size_str.strip('[]').split(',') + + # Convert each element to int and join with space + size_transformed = ' '.join(map(str, map(int, size_list))) + + return size_transformed + +def main(args): + csv_path = create_csv(args.input_folder,args.output_folder,output_csv=args.output_folder,name_csv="resample_csv.csv") + + with open(csv_path, mode='r') as csv_file: + csv_reader = csv.DictReader(csv_file) + + # Boucle sur chaque ligne du fichier CSV + for row in csv_reader: + # Afficher les informations de chaque ligne + size = tuple(map(int, row["size"].strip("()").split(","))) + input_path = row["in"] + out_path = row["out"] + print(size[1]) + print(f'Image d\'entrée: {row["in"]}, Image de sortie: {row["out"]}, Taille: {size}') + # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 768 576 768 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 119 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 443 --fit_spacing True --center 0 --iso_spacing 1 --linear False --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] + args_resample = create_args(img=input_path,out=out_path,size=args.resample_size,fit_spacing=True,center=0,iso_spacing=1,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + print("args resample : ",args_resample) + break + # subprocess.run(command,shell=True) + resample_images(args_resample) + + delete_csv(csv_path) + +def delete_csv(file_path): + """Delete a CSV file if it exists.""" + try: + if os.path.exists(file_path): + os.remove(file_path) + print(f"File {file_path} has been deleted successfully.") + else: + print(f"File {file_path} does not exist.") + except Exception as e: + print(f"An error occurred while trying to delete the file {file_path}: {e}") + + + + +if __name__=="__main__": + # SIZE AND SPACING TO RESAMPLE ARE HARD WRITTEN IN THE LINE 24 + parser = argparse.ArgumentParser(description='Get nifti info') + parser.add_argument('input_folder', type=str, help='Input path') + parser.add_argument('output_folder', type=str, help='Output path') + parser.add_argument('resample_size', type=str, help='size_resample') + # /home/luciacev/Documents/Gaelle/MultimodelRegistration/resample/resample.py + args = parser.parse_args() + + + main(args) \ No newline at end of file diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml new file mode 100644 index 0000000..5729369 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml @@ -0,0 +1,40 @@ + + + Automated Dental Tools.Advanced + 2 + MRI2CBCT_RESAMPLE_CBCT_MRI + + 0.0.1 + https://github.com/username/project + Slicer + FirstName LastName (Institution), FirstName LastName (Institution) + This work was partially funded by NIH grant NXNNXXNNNNNN-NNXN + + + + + + + + input_folder + + 0 + Path for the input. + + + + output_folder + + 1 + output folder + + + + resample_size + + 2 + size + + + + From e44f113b461ed6efe9c5376396ec95bb11fc3d29 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Mon, 15 Jul 2024 18:09:56 -0400 Subject: [PATCH 5/9] ENH : implement resample but bug + start normalization --- MRI2CBCT/MRI2CBCT.py | 177 +++++++++--- MRI2CBCT/utils/Preprocess_CBCT_MRI.py | 6 +- .../MRI2CBCT_CLI_utils/AREG_MRI_folder.py | 161 +++++++++++ MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py | 4 +- .../MRI2CBCT_CLI_utils/apply_mask_folder.py | 101 +++++++ .../MRI2CBCT_CLI_utils/mri_inverse.py | 52 ++++ .../normalize_percentile.py | 68 +++++ MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py | 219 ++++++++------ MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py | 269 +++++++++--------- MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml | 41 ++- .../MRI2CBCT_RESAMPLE_CBCT_MRI.py | 160 ++++------- .../MRI2CBCT_RESAMPLE_CBCT_MRI.xml | 25 +- 12 files changed, 889 insertions(+), 394 deletions(-) create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py diff --git a/MRI2CBCT/MRI2CBCT.py b/MRI2CBCT/MRI2CBCT.py index c370096..3890478 100644 --- a/MRI2CBCT/MRI2CBCT.py +++ b/MRI2CBCT/MRI2CBCT.py @@ -1,7 +1,7 @@ import logging import os from typing import Annotated, Optional -from qt import QApplication, QWidget, QTableWidget, QTableWidgetItem, QHeaderView,QSpinBox, QVBoxLayout, QLabel, QSizePolicy, QCheckBox, QFileDialog,QMessageBox, QApplication, QProgressDialog +from qt import QApplication, QWidget, QTableWidget, QDoubleSpinBox, QTableWidgetItem, QHeaderView,QSpinBox, QVBoxLayout, QLabel, QSizePolicy, QCheckBox, QFileDialog,QMessageBox, QApplication, QProgressDialog import qt from utils.Preprocess_CBCT import Process_CBCT from utils.Preprocess_MRI import Process_MRI @@ -281,52 +281,108 @@ def setup(self) -> None: 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 + # Increase the row and column count + self.tableWidgetResample.setRowCount(2) # Adding a second row + self.tableWidgetResample.setColumnCount(4) # Adding a new column + # 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) + 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"]) - + self.tableWidgetResample.setHorizontalHeaderLabels(["X", "Y", "Z", "Keep File"]) + self.tableWidgetResample.setVerticalHeaderLabels(["Number of slices", "Spacing"]) + + # Add QSpinBoxes for the first row + spinBox1 = QSpinBox() + spinBox1.setMaximum(10000) + spinBox1.setValue(119) + self.tableWidgetResample.setCellWidget(0, 0, spinBox1) + + spinBox2 = QSpinBox() + spinBox2.setMaximum(10000) + spinBox2.setValue(443) + self.tableWidgetResample.setCellWidget(0, 1, spinBox2) + + spinBox3 = QSpinBox() + spinBox3.setMaximum(10000) + spinBox3.setValue(443) + self.tableWidgetResample.setCellWidget(0, 2, spinBox3) + + # Add QSpinBoxes for the new row + spinBox4 = QDoubleSpinBox() + spinBox4.setMaximum(10000) + spinBox4.setSingleStep(0.1) + self.tableWidgetResample.setCellWidget(1, 0, spinBox4) + + spinBox5 = QDoubleSpinBox() + spinBox5.setMaximum(10000) + spinBox5.setSingleStep(0.1) + self.tableWidgetResample.setCellWidget(1, 1, spinBox5) + + spinBox6 = QDoubleSpinBox() + spinBox6.setMaximum(10000) + spinBox6.setSingleStep(0.1) + self.tableWidgetResample.setCellWidget(1, 2, spinBox6) + # Add QCheckBox for the "Keep File" column + checkBox1 = QCheckBox() + checkBox1.stateChanged.connect(lambda state: self.toggleSpinBoxes(state, [spinBox1, spinBox2, spinBox3])) + self.tableWidgetResample.setCellWidget(0, 3, checkBox1) + + checkBox2 = QCheckBox() + checkBox2.stateChanged.connect(lambda state: self.toggleSpinBoxes(state, [spinBox4, spinBox5, spinBox6])) + self.tableWidgetResample.setCellWidget(1, 3, checkBox2) - 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 toggleSpinBoxes(self, state, spinBoxes): + for spinBox in spinBoxes: + if state == 2: + spinBox.setEnabled(False) + spinBox.setStyleSheet("color: gray;") + else: + spinBox.setEnabled(True) + spinBox.setStyleSheet("") + def get_resample_values(self): """ Retrieves the resample values (X, Y, Z) from the QTableWidget. - :param tableWidgetResample: QTableWidget instance containing the resample values. - :return: A tuple of three integers representing the resample values (X, Y, Z). + :return: A tuple of two lists representing the resample values for the two rows. + Each list contains three values (X, Y, Z) or None if the "Keep File" checkbox is checked. """ - x_value = self.tableWidgetResample.cellWidget(0, 0).value - y_value = self.tableWidgetResample.cellWidget(0, 1).value - z_value = self.tableWidgetResample.cellWidget(0, 2).value - - return [x_value, y_value, z_value] + resample_values_row1 = [] + resample_values_row2 = [] + + # Check the "Keep File" checkbox for the first row + if self.tableWidgetResample.cellWidget(0, 3).isChecked(): + resample_values_row1 = "None" + else: + resample_values_row1 = [ + self.tableWidgetResample.cellWidget(0, 0).value, + self.tableWidgetResample.cellWidget(0, 1).value, + self.tableWidgetResample.cellWidget(0, 2).value + ] + + # Check the "Keep File" checkbox for the second row + if self.tableWidgetResample.cellWidget(1, 3).isChecked(): + resample_values_row2 = "None" + else: + resample_values_row2 = [ + self.tableWidgetResample.cellWidget(1, 0).value, + self.tableWidgetResample.cellWidget(1, 1).value, + self.tableWidgetResample.cellWidget(1, 2).value + ] + + return resample_values_row1, resample_values_row2 + def onCheckboxOrientClicked(self, row, col, state): @@ -591,9 +647,46 @@ def openFinder(self,nom : str,_) -> None : def onApplyButton(self) -> None: + self.list_Processes_Parameters=[] + # MRI2CBCT_ORIENT_CENTER_MRI + MRI2CBCT_RESAMPLE_REG = slicer.modules.mri2cbct_reg + parameter_mri2cbct_reg = { + "folder_general": self.ui.LineEditOutput.text, + "mri_folder": self.ui.lineEditRegMRI.text, + "cbct_folder": self.ui.lineEditRegCBCT.text, + "cbct_label2": self.ui.lineEditRegLabel.text, + "normalization" : [self.getNormalization()] + } + + self.list_Processes_Parameters.append( + { + "Process": MRI2CBCT_RESAMPLE_REG, + "Parameter": parameter_mri2cbct_reg, + "Module": "Resample files", + } + ) """Run processing when user clicks "Apply" button.""" print("get_normalization : ",self.getNormalization()) - print("getCheckboxValuesOrient : ",self.getCheckboxValuesOrient()) + + self.onProcessStarted() + + # /!\ Launch of the first process /!\ + print("module name : ",self.list_Processes_Parameters[0]["Module"]) + print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) + + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], + ) + + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + + del self.list_Processes_Parameters[0] + # print("getCheckboxValuesOrient : ",self.getCheckboxValuesOrient()) def downloadModel(self, lineEdit, name, test,_): @@ -749,10 +842,24 @@ def orientCenterMRI(self): def resampleMRICBCT(self): print("self.ui.lineEditOutputOrientMRI.text : ",self.ui.lineEditOuputResample.text) + if self.ui.comboBoxResample.currentText=="CBCT": + LineEditMRI = "None" + LineEditCBCT = self.ui.LineEditCBCT.text + elif self.ui.comboBoxResample.currentText=="MRI": + LineEditMRI = self.ui.LineEditMRI.text + LineEditCBCT = "None" + else : + LineEditMRI = self.ui.LineEditMRI.text + LineEditCBCT = self.ui.LineEditCBCT.text + + print("self.get_resample_values() : ",self.get_resample_values()) + self.list_Processes_Parameters = self.preprocess_mri_cbct.Process( - input_folder=self.ui.LineEditMRI.text, + input_folder_MRI=LineEditMRI, + input_folder_CBCT=LineEditCBCT, output_folder=self.ui.lineEditOuputResample.text, - resample_size=self.get_resample_values() + resample_size=self.get_resample_values()[0], + spacing=self.get_resample_values()[1] ) self.onProcessStarted() diff --git a/MRI2CBCT/utils/Preprocess_CBCT_MRI.py b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py index 8d4d508..3794808 100644 --- a/MRI2CBCT/utils/Preprocess_CBCT_MRI.py +++ b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py @@ -112,9 +112,11 @@ def Process(self, **kwargs): # MRI2CBCT_ORIENT_CENTER_MRI MRI2CBCT_RESAMPLE_CBCT_MRI = slicer.modules.mri2cbct_resample_cbct_mri parameter_mri2cbct_resample_cbct_mri = { - "input_folder": kwargs["input_folder"], + "input_folder_MRI": kwargs["input_folder_MRI"], + "input_folder_CBCT": kwargs["input_folder_CBCT"], "output_folder": kwargs["output_folder"], - "resample_size": kwargs["resample_size"] + "resample_size": kwargs["resample_size"], + "spacing" : kwargs["spacing"] } list_process.append( diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py new file mode 100644 index 0000000..0bd78a8 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py @@ -0,0 +1,161 @@ +import argparse +import os +import itk +import SimpleITK as sitk +from AREG_CBCT_utils.utils import ElastixApprox, MatrixRetrieval, ElastixReg, ComputeFinalMatrix, ResampleImage +import numpy as np + +def get_corresponding_file(folder, patient_id, modality): + """Get the corresponding file for a given patient ID and modality.""" + for root, _, files in os.walk(folder): + for file in files: + if file.startswith(patient_id) and modality in file and file.endswith(".nii.gz"): + return os.path.join(root, file) + return None + +def main(args): + cbct_folder = args.cbct_folder + mri_folder = args.mri_folder + cbct_mask_folder = args.cbct_mask_folder + mri_mask_folder = args.mri_mask_folder + cbct_seg_folder = args.cbct_seg_folder + mri_seg_folder = args.mri_seg_folder + output_folder = args.output_folder + mri_original_folder = args.mri_original_folder + cbct_original_folder = args.cbct_original_folder + + cbct_path = "None" + cbct_seg_path = "None" + mri_seg_path = "None" + + for cbct_file in os.listdir(cbct_folder): + if cbct_file.endswith(".nii.gz") and "_CBCT_" in cbct_file: + mri_mask_path="None" + patient_id = cbct_file.split("_CBCT_")[0] + + # cbct_path = os.path.join(cbct_folder, cbct_file) + cbct_path_original = get_corresponding_file(mri_original_folder, patient_id, "_CBCT_") + mri_path = get_corresponding_file(mri_folder, patient_id, "_MR_") + if mri_original_folder!="None": + mri_path_original = get_corresponding_file(mri_original_folder, patient_id, "_MR_") + + # if not mri_path: + # print(f"Corresponding MRI file for {cbct_file} not found.") + # continue + + cbct_mask_path = get_corresponding_file(cbct_mask_folder, patient_id, "_CBCT_") + # if mri_mask_folder!="None" : + # mri_mask_path = get_corresponding_file(mri_mask_folder, patient_id, "_MR_") + + # cbct_seg_path = get_corresponding_file(cbct_seg_folder, patient_id, "_CBCT_") + mri_seg_path = get_corresponding_file(mri_seg_folder, patient_id, "_MR_") + + # if not all([cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path]): + # print(f"One or more corresponding mask or segmentation files for {cbct_file} not found.") + # continue + print("-"*50) + # print("cbct_mask_path : ",cbct_mask_path) + print("mri_path : ",mri_path) + print("mri_path_original : ",mri_path_original) + print("mri_seg_path : ",mri_seg_path) + print("cbct_mask_path : ",cbct_mask_path) + # print("mri_path : ",mri_path) + + process_images(cbct_path, mri_path, cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path, output_folder,patient_id,mri_path_original,cbct_path_original) + +def process_images(cbct_path, mri_path, cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path, output_folder, patient_id,mri_path_original,cbct_path_original): + + + # cbct_path = itk.imread(cbct_path, itk.F) + try : + mri_path = itk.imread(mri_path, itk.F) + cbct_mask_path = itk.imread(cbct_mask_path, itk.F) + except KeyError as e: + print("An error occurred while reading the images") + print(e) + print(f"{patient_id} failed") + return + + Transforms = [] + + # TransformObj_Approx = np.eye(4) # 4x4 identity matrix + try : + TransformObj_Fine = ElastixReg(cbct_mask_path, mri_path, initial_transform=None) + except Exception as e: + print("An error occurred during the registration process:") + print(e) + print(f"{patient_id} failed") + return + + print(f"{patient_id} a ete traite") + transforms_Fine = MatrixRetrieval(TransformObj_Fine) + Transforms.append(transforms_Fine) + transform = ComputeFinalMatrix(Transforms) + + os.makedirs(output_folder, exist_ok=True) + + output_image_path = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg.nii.gz')) + output_image_path_transform = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg_transform.tfm')) + + sitk.WriteTransform(transform, output_image_path_transform) + + # resample_t2 = sitk.Cast(ResampleImage(sitk.ReadImage(mri_path_original), transform), sitk.sitkInt16) + # sitk.WriteImage(resample_t2, output_image_path) + + + # if mri_seg_path!="None": + # output_image_path_seg = os.path.join(output_folder,os.path.basename(mri_seg_path).replace('.nii.gz', f'_reg.nii.gz')) + # try : + # resample_seg = sitk.Cast(ResampleImage(sitk.ReadImage(mri_seg_path), transform), sitk.sitkInt16) + # sitk.WriteImage(resample_seg, output_image_path_seg) + + # except KeyError as e : + # print("Error to apply the matrix to : ",output_image_path_seg) + # print(e) + + + + + +def print_min_max(image, image_name): + image_array = itk.array_from_image(image) + min_value = image_array.min() + max_value = image_array.max() + print(f"{image_name} - Min: {min_value}, Max: {max_value}") + + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='AREG MRI folder') + + parser.add_argument("--cbct_folder", type=str, help="Folder containing CBCT images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm/test_percentile=[10,95]_norm=[0,75]") + parser.add_argument("--cbct_original_folder", type=str, help="Folder containing original CBCT.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b0_CBCT") + parser.add_argument("--cbct_mask_folder", type=str, help="Folder containing CBCT masks.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b3_CBCT_inv_norm_mask:l2/test_percentile=[10,95]_norm=[0,75]") + parser.add_argument("--cbct_seg_folder", type=str, help="Folder containing CBCT segmentations.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/d0_CBCT_seg_sep/label_2") + + parser.add_argument("--mri_folder", type=str, help="Folder containing MRI images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a2_MRI_inv_norm/test_percentile=[0,100]_norm=[0,100]") + parser.add_argument("--mri_original_folder", type=str, help="Folder containing original MRI.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a0_MRI") + parser.add_argument("--mri_mask_folder", type=str, help="Folder containing MRI masks.", default="None") + parser.add_argument("--mri_seg_folder", type=str, help="Folder containing MRI segmentations.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/c0_MRI_seg_sep/label_2") + + parser.add_argument("--output_folder", type=str, help="Folder to save the output files.",default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/z01_output/a03_mri:inv+norm[0,100]+p[0,100]_cbct:norm[0,75]+p[10,95]+mask") + + # parser.add_argument("--cbct_folder", type=str, help="Folder containing CBCT images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm/test_percentile=[10,95]_norm=[0,75]") + # parser.add_argument("--cbct_original_folder", type=str, help="Folder containing original CBCT.", default="None") + # parser.add_argument("--cbct_mask_folder", type=str, help="Folder containing CBCT masks.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b3_CBCT_inv_norm_mask:l2/test_percentile=[10,95]_norm=[0,75]") + # parser.add_argument("--cbct_seg_folder", type=str, help="Folder containing CBCT segmentations.", default="None") + + # parser.add_argument("--mri_folder", type=str, help="Folder containing MRI images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a2_MRI_inv_norm/test_percentile=[0,100]_norm=[0,100]") + # parser.add_argument("--mri_original_folder", type=str, help="Folder containing original MRI.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a0_MRI") + # parser.add_argument("--mri_mask_folder", type=str, help="Folder containing MRI masks.", default="None") + # parser.add_argument("--mri_seg_folder", type=str, help="Folder containing MRI segmentations.", default="None") + + # parser.add_argument("--output_folder", type=str, help="Folder to save the output files.",default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/z01_output/a02_mri:inv+norm[0,100]+p[0,100]_cbct:norm[0,75]+p[10,95]+mask") + + args = parser.parse_args() + + if not os.path.exists(args.output_folder): + os.makedirs(args.output_folder) + + main(args) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py index 4913410..1378ee9 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py @@ -1,6 +1,6 @@ from .resample_create_csv import ( - create_csv + create_csv, ) # from .Net import DenseNet -from .resample import resample_images +from .resample import resample_images, run_resample diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py new file mode 100644 index 0000000..a8a6dc0 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py @@ -0,0 +1,101 @@ +import SimpleITK as sitk +import os +import argparse +import numpy as np + + +def MaskedImage(fixed_image_path, fixed_seg_path, folder_output, suffix, SegLabel=None): + """Mask the fixed image with the fixed segmentation and write it to a file""" + # print("fixed_seg_path : ",fixed_seg_path) + # print("fixed_image_path : ",fixed_image_path) + fixed_image_sitk = sitk.ReadImage(fixed_image_path) + fixed_seg_sitk = sitk.ReadImage(fixed_seg_path) + # fixed_seg_sitk.SetOrigin(fixed_image_sitk.GetOrigin()) + + fixed_image_masked = applyMask(fixed_image_sitk, fixed_seg_sitk, label=SegLabel) + if fixed_image_masked=="failed": + print("failed process on : ",fixed_image_sitk) + return + + # Write the masked image + base_name, ext = os.path.splitext(fixed_image_path) + if base_name.endswith('.nii'): # Case for .nii.gz + ext = '.nii.gz' + + file_name = os.path.basename(fixed_image_path) + file_name_without_ext = os.path.splitext(os.path.splitext(file_name)[0])[0] + + # Construction du chemin de fichier de sortie + output_path = os.path.join(folder_output, f"{file_name_without_ext}_{suffix}{ext}") + print("-"*100) + print("output_path : ",output_path) + sitk.WriteImage(sitk.Cast(fixed_image_masked, sitk.sitkInt16), output_path) + + return output_path + + +def applyMask(image, mask, label): + """Apply a mask to an image.""" + try : + array = sitk.GetArrayFromImage(mask) + if label is not None and label in np.unique(array): + array = np.where(array == label, 1, 0) + mask = sitk.GetImageFromArray(array) + mask.CopyInformation(image) + except KeyError as e : + print(e) + return "failed" + + return sitk.Mask(image, mask) + + +def find_segmentation_file(image_file, seg_folder): + """Find the corresponding segmentation file for a given image file.""" + base_name = os.path.basename(image_file) + patient_id = base_name.split('_CBCT')[0].split('_MR')[0] + # print("patient_id : ",patient_id) + # print("base_name : ",base_name) + + for seg_file in os.listdir(seg_folder): + if seg_file.startswith(patient_id) and seg_file.endswith('_label2.nii.gz'): + return os.path.join(seg_folder, seg_file) + + return None + + +def process_folder(folder_path, seg_folder, folder_output, suffix, seg_label): + """Process all files in the specified folder.""" + for root, _, files in os.walk(folder_path): + for file in files: + if file.endswith(('.nii', '.nii.gz')) and ('_CBCT' in file or '_MR' in file): + fixed_image_path = os.path.join(root, file) + fixed_seg_path = find_segmentation_file(file, seg_folder) + + if fixed_seg_path: + # print(f"Processing file: {fixed_image_path}") + try : + MaskedImage(fixed_image_path, fixed_seg_path, folder_output, suffix, seg_label) + # print(f"Segmentation file for {fixed_image_path} succedeed.") + except KeyError as e: + print(f"Segmentation file for {fixed_image_path} failed.") + print(e) + continue + else: + print(f"Segmentation file for {fixed_image_path} not found.") + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Apply segmentation mask to all MRI files in a folder.") + parser.add_argument("--folder_path", type=str, default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm/test_percentile=[10,95]_norm=[0,75]", help="The path to the folder containing the MRI files.") + parser.add_argument("--seg_folder", type=str, default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/d0_CBCT_seg_sep/label_2", help="The path to the segmentation file.") + parser.add_argument("--folder_output", type=str, default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b3_CBCT_inv_norm_mask:l2/a03_test_percentile=[10,95]_norm=[0,75]", help="The path to the output folder for the masked files.") + parser.add_argument("--suffix", type=str, default="mask", help="The suffix to add to the output filenames.") + parser.add_argument("--seg_label", type=int, default=1, help="Label of the segmentation.") + + args = parser.parse_args() + + if not os.path.exists(args.folder_output): + os.makedirs(args.folder_output) + + process_folder(args.folder_path, args.seg_folder, args.folder_output, args.suffix, args.seg_label) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py new file mode 100644 index 0000000..86796f3 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py @@ -0,0 +1,52 @@ +import SimpleITK as sitk +import os +import argparse + +def invert_mri_intensity(path_folder, folder_output, suffix): + # Check if the output folder exists, if not create it + if not os.path.exists(folder_output): + os.makedirs(folder_output) + + # Iterate through the files in the input folder + for filename in os.listdir(path_folder): + if filename.endswith('.nii') or filename.endswith('.nii.gz'): + filepath = os.path.join(path_folder, filename) + image = sitk.ReadImage(filepath) + + # Convert the image to a numpy array to manipulate intensities + image_array = sitk.GetArrayFromImage(image) + + # Find the maximum intensity value in the image + max_intensity = image_array.max() + + # Invert the intensities while keeping the background (where intensity is 0) unchanged + inverted_image_array = max_intensity - image_array + inverted_image_array[image_array == 0] = 0 + + # Convert the inverted numpy array back to a SimpleITK image + inverted_image = sitk.GetImageFromArray(inverted_image_array) + + # Copy the original image information (such as spacing, origin, etc.) to the inverted image + inverted_image.CopyInformation(image) + + # Generate the new filename with the suffix + base_name, ext = os.path.splitext(filename) + if base_name.endswith('.nii'): # Case for .nii.gz + base_name, ext2 = os.path.splitext(base_name) + ext = ext2 + ext + + output_filename = os.path.join(folder_output, f"{base_name}_{suffix}{ext}") + + # Save the inverted image + sitk.WriteImage(inverted_image, output_filename) + + print(f"Inversion completed for {filename}, saved as {output_filename}") + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Invert the intensity of MRI images while keeping the background at 0.") + parser.add_argument("--path_folder", type=str, help="The path to the folder containing the MRI files", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a0_MRI") + parser.add_argument("--folder_output", type=str, help="The path to the output folder for the inverted files",default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a1_MRI_inv") + parser.add_argument("--suffix", type=str, help="The suffix to add to the output filenames",default="inv") + + args = parser.parse_args() + invert_mri_intensity(args.path_folder, args.folder_output, args.suffix) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py new file mode 100644 index 0000000..0780843 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py @@ -0,0 +1,68 @@ +import argparse +import os +import SimpleITK as sitk +import numpy as np + +def compute_thresholds(image, lower_percentile=10, upper_percentile=90): + """Compute thresholds based on percentiles of the image histogram.""" + array = sitk.GetArrayFromImage(image) + lower_threshold = np.percentile(array, lower_percentile) + upper_threshold = np.percentile(array, upper_percentile) + return lower_threshold, upper_threshold + +def enhance_contrast(image,upper_percentile,lower_percentile, min_norm, max_norm): + """Enhance the contrast of the image while keeping normalization between 0 and 1.""" + # Compute thresholds + lower_threshold, upper_threshold = compute_thresholds(image,lower_percentile,upper_percentile) + # print(f"Computed thresholds - Lower: {lower_threshold}, Upper: {upper_threshold}") + + + # Normalize the image using the computed thresholds + array = sitk.GetArrayFromImage(image) + normalized_array = np.clip((array - lower_threshold) / (upper_threshold - lower_threshold), 0, 1) + scaled_array = normalized_array * max_norm - min_norm + + return sitk.GetImageFromArray(scaled_array) + +def process_images(input_folder, output_folder,upper_percentile,lower_percentile,min_norm, max_norm): + """Process all .nii.gz images in the input folder.""" + if not os.path.exists(output_folder): + os.makedirs(output_folder) + + for filename in os.listdir(input_folder): + if filename.endswith('.nii.gz'): + input_path = os.path.join(input_folder, filename) + print("filename : ",filename) + img = sitk.ReadImage(input_path) + + # Enhance the contrast of the image + enhanced_img = enhance_contrast(img,upper_percentile,lower_percentile,min_norm, max_norm) + + # Copy original metadata to the enhanced image + enhanced_img.CopyInformation(img) + + # Save the enhanced image with the new suffix + output_filename = filename.replace('.nii.gz', f'_percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}].nii.gz') + output_path = os.path.join(output_folder, output_filename) + sitk.WriteImage(enhanced_img, output_path) + print(f'Saved enhanced image to {output_path}') + +def main(): + parser = argparse.ArgumentParser(description='Enhance contrast of NIfTI images and save with a new suffix.') + parser.add_argument('--input_folder', type=str, help='Path to the input folder containing .nii.gz images.', default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b0_CBCT") + parser.add_argument('--output_folder', type=str, help='Path to the output folder to save normalized images.', default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm") + parser.add_argument('--upper_percentile', type=int, help='upper percentile to apply, choose between 0 and 100',default=95) + parser.add_argument('--lower_percentile', type=int, help='lower percentile to apply, choose between 0 and 100',default=10) + parser.add_argument('--max_norm', type=int, help='max value after normalization',default=75) + parser.add_argument('--min_norm', type=int, help='min value after normalization',default=0) + + args = parser.parse_args() + + output_path = os.path.join(args.output_folder,f"test_percentile=[{args.lower_percentile},{args.upper_percentile}]_norm=[{args.min_norm},{args.max_norm}]") + if not os.path.exists(output_path): + os.makedirs(output_path) + + process_images(args.input_folder, output_path, args.upper_percentile,args.lower_percentile,args.min_norm, args.max_norm) + +if __name__ == '__main__': + main() diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py index 64da3f9..0112f99 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py @@ -7,13 +7,14 @@ import csv def resample_fn(img, args): - output_size = args.size - fit_spacing = args.fit_spacing - iso_spacing = args.iso_spacing - pixel_dimension = args.pixel_dimension - center = args.center - - if args.linear: + output_size = args['size'] + fit_spacing = args['fit_spacing'] + iso_spacing = args['iso_spacing'] + print("iso_spacing : ",iso_spacing) + pixel_dimension = args['pixel_dimension'] + center = args['center'] + + if args['linear']: InterpolatorType = sitk.sitkLinear else: InterpolatorType = sitk.sitkNearestNeighbor @@ -32,19 +33,19 @@ def resample_fn(img, args): output_spacing = spacing - if(iso_spacing): - output_spacing_filtered = [sp for si, sp in zip(args.size, output_spacing) if si != -1] + if(iso_spacing=="True"): + output_spacing_filtered = [sp for si, sp in zip(args['size'], output_spacing) if si != -1] # print(output_spacing_filtered) max_spacing = np.max(output_spacing_filtered) - output_spacing = [sp if si == -1 else max_spacing for si, sp in zip(args.size, output_spacing)] + output_spacing = [sp if si == -1 else max_spacing for si, sp in zip(args['size'], output_spacing)] # print(output_spacing) - if(args.spacing is not None): - output_spacing = args.spacing + if(args['spacing'] is not None): + output_spacing = args['spacing'] - if(args.origin is not None): - output_origin = args.origin + if(args['origin'] is not None): + output_origin = args['origin'] if(center): output_physical_size = np.array(output_size)*np.array(output_spacing) @@ -71,60 +72,60 @@ def resample_fn(img, args): def Resample(img_filename, args): - output_size = args.size - fit_spacing = args.fit_spacing - iso_spacing = args.iso_spacing - img_dimension = args.image_dimension - pixel_dimension = args.pixel_dimension + output_size = args['size'] + fit_spacing = args['fit_spacing'] + iso_spacing = args['iso_spacing'] + img_dimension = args['image_dimension'] + pixel_dimension = args['pixel_dimension'] print("Reading:", img_filename) img = sitk.ReadImage(img_filename) - if(args.img_spacing): - img.SetSpacing(args.img_spacing) + if(args['img_spacing']): + img.SetSpacing(args['img_spacing']) return resample_fn(img, args) def resample_images(args): filenames = [] - if args.img: - fobj = {"img": args.img, "out": args.out} + if args['img']: + fobj = {"img": args['img'], "out": args['out']} filenames.append(fobj) - elif args.dir: - out_dir = args.out - normpath = os.path.normpath("/".join([args.dir, '**', '*'])) + elif args['dir']: + out_dir = args['out'] + normpath = os.path.normpath("/".join([args['dir'], '**', '*'])) for img in glob.iglob(normpath, recursive=True): if os.path.isfile(img) and any(ext in img for ext in [".nrrd", ".nii", ".nii.gz", ".mhd", ".dcm", ".DCM", ".jpg", ".png"]): - fobj = {"img": img, "out": os.path.normpath(out_dir + "/" + img.replace(args.dir, ''))} - if args.out_ext is not None: - out_ext = args.out_ext if args.out_ext.startswith(".") else "." + args.out_ext + fobj = {"img": img, "out": os.path.normpath(out_dir + "/" + img.replace(args['dir'], ''))} + if args['out_ext'] is not None: + out_ext = args['out_ext'] if args['out_ext'].startswith(".") else "." + args['out_ext'] fobj["out"] = os.path.splitext(fobj["out"])[0] + out_ext if not os.path.exists(os.path.dirname(fobj["out"])): os.makedirs(os.path.dirname(fobj["out"])) if not os.path.exists(fobj["out"]) or args.ow: filenames.append(fobj) - elif args.csv: - replace_dir_name = args.csv_root_path - with open(args.csv) as csvfile: + elif args['csv']: + replace_dir_name = args['csv_root_path'] + with open(args['csv']) as csvfile: csv_reader = csv.DictReader(csvfile) for row in csv_reader: - fobj = {"img": row[args.csv_column], "out": row[args.csv_column]} + fobj = {"img": row[args['csv_column']], "out": row[args['csv_column']]} if replace_dir_name: - fobj["out"] = fobj["out"].replace(replace_dir_name, args.out) - if args.csv_use_spc: + fobj["out"] = fobj["out"].replace(replace_dir_name, args['out']) + if args['csv_use_spc']: img_spacing = [ - row[args.csv_column_spcx] if args.csv_column_spcx else None, - row[args.csv_column_spcy] if args.csv_column_spcy else None, - row[args.csv_column_spcz] if args.csv_column_spcz else None, + row[args['csv_column_spcx']] if args['csv_column_spcx'] else None, + row[args['csv_column_spcy']] if args['csv_column_spcy'] else None, + row[args['csv_column_spcz']] if args['csv_column_spcz'] else None, ] fobj["img_spacing"] = [spc for spc in img_spacing if spc] if "ref" in row: fobj["ref"] = row["ref"] - if args.out_ext is not None: - out_ext = args.out_ext if args.out_ext.startswith(".") else "." + args.out_ext + if args['out_ext'] is not None: + out_ext = args['out_ext'] if args['out_ext'].startswith(".") else "." + args['out_ext'] fobj["out"] = os.path.splitext(fobj["out"])[0] + out_ext if not os.path.exists(os.path.dirname(fobj["out"])): os.makedirs(os.path.dirname(fobj["out"])) @@ -133,30 +134,30 @@ def resample_images(args): else: raise ValueError("Set img or dir to resample!") - if args.rgb: - if args.pixel_dimension == 3: + if args['rgb']: + if args['pixel_dimension'] == 3: print("Using: RGB type pixel with unsigned char") - elif args.pixel_dimension == 4: + elif args['pixel_dimension'] == 4: print("Using: RGBA type pixel with unsigned char") else: print("WARNING: Pixel size not supported!") - if args.ref is not None: - print(args.ref) - ref = sitk.ReadImage(args.ref) - args.size = ref.GetSize() - args.spacing = ref.GetSpacing() - args.origin = ref.GetOrigin() + if args['ref'] is not None: + print(args['ref']) + ref = sitk.ReadImage(args['ref']) + args['size'] = ref.GetSize() + args['spacing'] = ref.GetSpacing() + args['origin'] = ref.GetOrigin() for fobj in filenames: try: if "ref" in fobj and fobj["ref"] is not None: ref = sitk.ReadImage(fobj["ref"]) - args.size = ref.GetSize() - args.spacing = ref.GetSpacing() - args.origin = ref.GetOrigin() + args['size'] = ref.GetSize() + args['spacing'] = ref.GetSpacing() + args['origin'] = ref.GetOrigin() - if args.size is not None: + if args['size'] is not None: img = Resample(fobj["img"], args) else: img = sitk.ReadImage(fobj["img"]) @@ -170,45 +171,79 @@ def resample_images(args): except Exception as e: print(e, file=sys.stderr) -def main(): - parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - - in_group = parser.add_mutually_exclusive_group(required=True) - in_group.add_argument('--img', type=str, help='image to resample') - in_group.add_argument('--dir', type=str, help='Directory with image to resample') - in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') - - csv_group = parser.add_argument_group('CSV extra parameters') - csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') - csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') - csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') - csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') - csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') - csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') - - transform_group = parser.add_argument_group('Transform parameters') - transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) - transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) - transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') - transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') - transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') - transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) - transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) - transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) - transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) - - img_group = parser.add_argument_group('Image parameters') - img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) - img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) - img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) - - out_group = parser.add_argument_group('Output parameters') - out_group.add_argument('--ow', type=int, help='Overwrite', default=1) - out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") - out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) - - args = parser.parse_args() +# def main(): +# parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + +# in_group = parser.add_mutually_exclusive_group(required=True) +# in_group.add_argument('--img', type=str, help='image to resample') +# in_group.add_argument('--dir', type=str, help='Directory with image to resample') +# in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') + +# csv_group = parser.add_argument_group('CSV extra parameters') +# csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') +# csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') +# csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') +# csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') +# csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') +# csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') + +# transform_group = parser.add_argument_group('Transform parameters') +# transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) +# transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) +# transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') +# transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') +# transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') +# transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) +# transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) +# transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) +# transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) + +# img_group = parser.add_argument_group('Image parameters') +# img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) +# img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) +# img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) + +# out_group = parser.add_argument_group('Output parameters') +# out_group.add_argument('--ow', type=int, help='Overwrite', default=1) +# out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") +# out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) + +# args = parser.parse_args() +# resample_images(args) + + +def run_resample(img=None, dir=None, csv=None, csv_column='image', csv_root_path=None, csv_use_spc=0, + csv_column_spcx=None, csv_column_spcy=None, csv_column_spcz=None, ref=None, size=None, + img_spacing=None, spacing=None, origin=None, linear=False, center=0, fit_spacing=False, + iso_spacing=False, image_dimension=2, pixel_dimension=1, rgb=False, ow=1, out="./out.nrrd", + out_ext=None): + args = { + 'img': img, + 'dir': dir, + 'csv': csv, + 'csv_column': csv_column, + 'csv_root_path': csv_root_path, + 'csv_use_spc': csv_use_spc, + 'csv_column_spcx': csv_column_spcx, + 'csv_column_spcy': csv_column_spcy, + 'csv_column_spcz': csv_column_spcz, + 'ref': ref, + 'size': size, + 'img_spacing': img_spacing, + 'spacing': spacing, + 'origin': origin, + 'linear': linear, + 'center': center, + 'fit_spacing': fit_spacing, + 'iso_spacing': iso_spacing, + 'image_dimension': image_dimension, + 'pixel_dimension': pixel_dimension, + 'rgb': rgb, + 'ow': ow, + 'out': out, + 'out_ext': out_ext, + } resample_images(args) if __name__ == "__main__": - main() \ No newline at end of file + run_resample() \ No newline at end of file diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py index 02d9978..679d181 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py @@ -1,133 +1,146 @@ #!/usr/bin/env python-real +import subprocess import argparse -import SimpleITK as sitk -import sys, os, time -import numpy as np - -fpath = os.path.join(os.path.dirname(__file__), "..") -sys.path.append(fpath) - -from ASO_CBCT_utils import ( - ExtractFilesFromFolder, - AngleAndAxisVectors, - RotationMatrix, - PreASOResample, - convertdicom2nifti, -) - - -def ResampleImage(image, transform): - """ - Resample image using SimpleITK - - Parameters - ---------- - image : SimpleITK.Image - Image to be resampled - target : SimpleITK.Image - Target image - transform : SimpleITK transform - Transform to be applied to the image. - - Returns - ------- - SimpleITK image - Resampled image. - """ - resample = sitk.ResampleImageFilter() - resample.SetReferenceImage(image) - resample.SetTransform(transform) - resample.SetInterpolator(sitk.sitkLinear) - orig_size = np.array(image.GetSize(), dtype=int) - ratio = 1 - new_size = orig_size * ratio - new_size = np.ceil(new_size).astype(int) # Image dimensions are in integers - new_size = [int(s) for s in new_size] - resample.SetSize(new_size) - resample.SetDefaultPixelValue(0) - - # Set New Origin - orig_origin = np.array(image.GetOrigin()) - # apply transform to the origin - orig_center = np.array( - image.TransformContinuousIndexToPhysicalPoint(np.array(image.GetSize()) / 2.0) - ) - # new_center = np.array(target.TransformContinuousIndexToPhysicalPoint(np.array(target.GetSize())/2.0)) - new_origin = orig_origin - orig_center - resample.SetOutputOrigin(new_origin) - - return resample.Execute(image) - - -def main(args): - - input_dir, out_dir, smallFOV, isDCMInput = ( - os.path.normpath(args.input[0]), - os.path.normpath(args.output_folder[0]), - args.SmallFOV[0] == "true", - args.DCMInput[0] == "true", - ) - - if isDCMInput: - convertdicom2nifti(input_dir) - - scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] - - if not os.path.exists(out_dir): - os.makedirs(out_dir) - - input_files, _ = ExtractFilesFromFolder(input_dir, scan_extension) - - for i in range(len(input_files)): - - input_file = input_files[i] - - img = sitk.ReadImage(input_file) - - # Translation to center volume - T = -np.array( - img.TransformContinuousIndexToPhysicalPoint(np.array(img.GetSize()) / 2.0) - ) - translation = sitk.TranslationTransform(3) - translation.SetOffset(T.tolist()) - - img_trans = ResampleImage(img, translation.GetInverse()) - img_out = img_trans - - # Write Scan - dir_scan = os.path.dirname(input_file.replace(input_dir, out_dir)) - if not os.path.exists(dir_scan): - os.makedirs(dir_scan) - - file_outpath = os.path.join(dir_scan, os.path.basename(input_file)) - if not os.path.exists(file_outpath): - sitk.WriteImage(img_out, file_outpath) - - print(f"""{0}""") - sys.stdout.flush() - time.sleep(0.2) - print(f"""{2}""") - sys.stdout.flush() - time.sleep(0.2) - print(f"""{0}""") - sys.stdout.flush() - time.sleep(0.2) - - -if __name__ == "__main__": - - print("PRE ASO") - - parser = argparse.ArgumentParser() - - parser.add_argument("input", nargs=1) - parser.add_argument("output_folder", nargs=1) - parser.add_argument("model_folder", nargs=1) - parser.add_argument("SmallFOV", nargs=1) - parser.add_argument("temp_folder", nargs=1) - parser.add_argument("DCMInput", nargs=1) - +import os +import re + +def create_folder(folder): + if not os.path.exists(folder): + os.makedirs(folder) + +def run_script(script_name, args): + command = ['python', script_name] + args + result = subprocess.run(command, capture_output=True, text=True) + print(f"Running {script_name} with arguments {args}") + print("Output:\n", result.stdout) + if result.stderr: + print("Errors:\n", result.stderr) + +def run_script_inverse_mri(mri_folder, folder_general): + folder_mri_inverse = os.path.join(folder_general,"a01_MRI_inv") + create_folder(folder_mri_inverse) + script_name = 'mri_inverse.py' + args = [ + f"--path_folder={mri_folder}", + f"--folder_output={folder_mri_inverse}", + f"--suffix=inv", + ] + run_script(script_name, args) + return folder_mri_inverse + +def run_script_normalize_percentile(file_type,input_folder, folder_general, upper_percentile, lower_percentile, max_norm, min_norm): + script_name = 'normalize_percentile.py' + if file_type=="MRI": + output_folder_norm = os.path.join(folder_general,"a2_MRI_inv_norm") + else : + output_folder_norm = os.path.join(folder_general,"b2_CBCT_norm") + create_folder(output_folder_norm) + args = [ + f"--input_folder={input_folder}", + f"--output_folder={output_folder_norm}", + f"--upper_percentile={upper_percentile}", + f"--lower_percentile={lower_percentile}", + f"--max_norm={max_norm}", + f"--min_norm={min_norm}" + ] + run_script(script_name, args) + output_path_norm = os.path.join(output_folder_norm,f"test_percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") + return output_path_norm + + +def run_script_apply_mask(cbct_folder, cbct_label2,folder_general, suffix,upper_percentile, lower_percentile, max_norm, min_norm): + script_name = 'apply_mask_folder.py' + cbct_mask_folder = os.path.join(folder_general,"b3_CBCT_norm_mask:l2",f"test_percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") + create_folder(cbct_mask_folder) + args = [ + f"--folder_path={cbct_folder}", + f"--seg_folder={cbct_label2}", + f"--folder_output={cbct_mask_folder}", + f"--suffix={suffix}", + f"--seg_label={1}" + ] + run_script(script_name, args) + return cbct_mask_folder + +def run_script_AREG_MRI_folder(cbct_folder, cbct_mask_folder,mri_folder,mri_original_folder,folder_general,mri_lower_p,mri_upper_p,mri_min_norm,mri_max_norm,cbct_lower_p,cbct_upper_p,cbct_min_norm,cbct_max_norm): + script_name = 'AREG_MRI_folder.py' + output_folder = os.path.join(folder_general,"z01_output",f"a01_mri:inv+norm[{mri_min_norm},{mri_max_norm}]+p[{mri_lower_p},{mri_upper_p}]_cbct:norm[{cbct_min_norm},{cbct_max_norm}]+p[{cbct_lower_p},{cbct_upper_p}]+mask") + create_folder(output_folder) + args = [ + f"--cbct_folder={cbct_folder}", + f"--cbct_original_folder=None", + f"--cbct_mask_folder={cbct_mask_folder}", + f"--cbct_seg_folder=None", + f"--mri_folder={mri_folder}", + f"--mri_original_folder={mri_original_folder}", + f"--mri_mask_folder=None", + f"--mri_seg_folder=None", + f"--output_folder={output_folder}" + ] + run_script(script_name, args) + return cbct_mask_folder + +def extract_values(input_string): + # Utiliser une expression régulière pour extraire tous les chiffres + numbers = re.findall(r'\d+', input_string) + + # Convertir les nombres extraits en entiers + numbers = list(map(int, numbers)) + + # Vérifier qu'il y a exactement 8 nombres + if len(numbers) != 8: + raise ValueError("L'entrée doit contenir exactement 8 chiffres.") + + # Assigner les nombres à des variables séparées + a, b, c, d, e, f, g, h = numbers + + return a, b, c, d, e, f, g, h + +def main(): + parser = argparse.ArgumentParser(description="Run multiple Python scripts with arguments") + parser.add_argument('folder_general', type=str, help="Folder general where to make all the output") + parser.add_argument('mri_folder', type=str, help="Folder containing original MRI images.") + parser.add_argument('cbct_folder', type=str, help="Folder containing original CBCT images.") + parser.add_argument('cbct_label2', type=str, help="Folder containing CBCT masks.") + parser.add_argument('normalization', type=str, help="Folder containing CBCT masks.") args = parser.parse_args() + print("normalization : ",args.normalization) + print("type normalization : ",type(args.normalization)) + + mri_min_norm, mri_max_norm, mri_lower_p, mri_upper_p, cbct_min_norm, cbct_max_norm, cbct_lower_p, cbct_upper_p = extract_values(args.normalization) + print(f"mri_lower_p: {mri_lower_p}") + print(f"mri_upper_p: {mri_upper_p}") + print(f"mri_min_norm: {mri_min_norm}") + print(f"mri_max_norm: {mri_max_norm}") + + print(f"cbct_lower_p: {cbct_lower_p}") + print(f"cbct_upper_p: {cbct_upper_p}") + print(f"cbct_min_norm: {cbct_min_norm}") + print(f"cbct_max_norm: {cbct_max_norm}") + + # Appel des fonctions une par une + # mri_lower_p = 10 + # mri_upper_p = 95 + # mri_min_norm = 0 + # mri_max_norm = 100 + + # cbct_lower_p = 10 + # cbct_upper_p = 95 + # cbct_min_norm = 0 + # cbct_max_norm = 150 + # # MRI + # folder_mri_inverse = run_script_inverse_mri(args.mri_folder, args.folder_general) + # input_path_norm_mri = run_script_normalize_percentile("MRI",folder_mri_inverse, args.folder_general, upper_percentile=mri_upper_p, lower_percentile=mri_lower_p, max_norm=mri_max_norm, min_norm=mri_min_norm) + + # # CBCT + # output_path_norm_cbct = run_script_normalize_percentile("CBCT",args.cbct_folder, args.folder_general, upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) + # input_path_cbct_norm_mask = run_script_apply_mask(output_path_norm_cbct,args.cbct_label2,args.folder_general,"mask",upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) + + # # REG + # run_script_AREG_MRI_folder(cbct_folder=args.cbct_folder,cbct_mask_folder=input_path_cbct_norm_mask,mri_folder=input_path_norm_mri,mri_original_folder=args.mri_folder,folder_general=args.folder_general,mri_lower_p=mri_lower_p,mri_upper_p=mri_upper_p,mri_min_norm=mri_min_norm,mri_max_norm=mri_max_norm,cbct_lower_p=cbct_lower_p,cbct_upper_p=cbct_upper_p,cbct_min_norm=cbct_min_norm,cbct_max_norm=cbct_max_norm) + + - main(args) +if __name__ == "__main__": + main() diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml index 0b5d3a9..7d53ee0 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml @@ -16,45 +16,38 @@ - input - + folder_general + 0 - Path for the input. + output_folder - output_folder - + mri_folder + 1 - Output Path + Input Path MRI folder - model_folder - + cbct_folder + 2 - Folder with model files + Input Path CBCT folder - SmallFOV - - 4 - Boolean to say whether or not the input file is a Small FOV + cbct_label2 + + 3 + Input path CB CBCT label - temp_folder - - 5 - Temp folder for pre aso resample for lightning models - - - - DCMInput - - 6 - Is Dicom as Input + normalization + + 4 + Normalization to use for MRI and CBCT diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py index fdd2951..bafc887 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py @@ -1,15 +1,9 @@ #!/usr/bin/env python-real -import subprocess import csv import argparse import os -# from MRI2CBCT_CLI import MRI2CBCT_CLI_utils -# from MRI2CBCT_CLI_utils import ( -# create_csv, -# resample_images, - -# ) + import sys fpath = os.path.join(os.path.dirname(__file__), "..") sys.path.append(fpath) @@ -17,85 +11,40 @@ from MRI2CBCT_CLI_utils import create_csv, resample_images import csv -# THE PACING IS CHOOSEN RUNING CALCUL_SPACING_MEAN.PY DONC LA MOYENNE DES SPACING QUE ON A - -def run_resample(args): - # Remplacez ceci par le chemin vers votre fichier CSV - csv_file_path = args.csv - # Ouvrir le fichier CSV en lecture - #RESAMPLE - with open(csv_file_path, mode='r') as csv_file: - csv_reader = csv.DictReader(csv_file) - - # Boucle sur chaque ligne du fichier CSV - for row in csv_reader: - # Afficher les informations de chaque ligne - size = tuple(map(int, row["size"].strip("()").split(","))) - input_path = row["in"] - out_path = row["out"] - print(size[1]) - print(f'Image d\'entrée: {row["in"]}, Image de sortie: {row["out"]}, Taille: {size}') - # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 768 576 768 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 119 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 443 --fit_spacing True --center 0 --iso_spacing 1 --linear False --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - subprocess.run(command,shell=True) - -def create_args(img=None, dir=None, csv=None, csv_column='image', csv_root_path=None, csv_use_spc=0, - csv_column_spcx=None, csv_column_spcy=None, csv_column_spcz=None, ref=None, size=None, - img_spacing=None, spacing=None, origin=None, linear=False, center=0, fit_spacing=False, - iso_spacing=False, image_dimension=2, pixel_dimension=1, rgb=False, ow=1, out="./out.nrrd", - out_ext=None): - parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - in_group = parser.add_mutually_exclusive_group(required=True) - in_group.add_argument('--img', type=str, help='image to resample') - in_group.add_argument('--dir', type=str, help='Directory with image to resample') - in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') - - csv_group = parser.add_argument_group('CSV extra parameters') - csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') - csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') - csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') - csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') - csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') - csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') - - transform_group = parser.add_argument_group('Transform parameters') - transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) - transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) - transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') - transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') - transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') - transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) - transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) - transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) - transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) - - img_group = parser.add_argument_group('Image parameters') - img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) - img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) - img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) - - out_group = parser.add_argument_group('Output parameters') - out_group.add_argument('--ow', type=int, help='Overwrite', default=1) - out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") - out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) - # Manually set the args - print(transform_size(size)) - args = parser.parse_args(args=[ - '--img', img, - '--size', "119 443 443", - '--linear', str(linear), - '--center', str(center), - '--fit_spacing', str(fit_spacing), - '--iso_spacing', str(iso_spacing), - '--image_dimension', str(image_dimension), - '--pixel_dimension', str(pixel_dimension), - '--rgb', str(rgb), - '--ow', str(ow), - '--out', out, - ]) - return args +def run_resample(img=None, dir=None, csv=None, csv_column='image', csv_root_path=None, csv_use_spc=0, + csv_column_spcx=None, csv_column_spcy=None, csv_column_spcz=None, ref=None, size=None, + img_spacing=None, spacing=None, origin=None, linear=False, center=0, fit_spacing=False, + iso_spacing=False, image_dimension=2, pixel_dimension=1, rgb=False, ow=1, out="./out.nrrd", + out_ext=None): + args = { + 'img': img, + 'dir': dir, + 'csv': csv, + 'csv_column': csv_column, + 'csv_root_path': csv_root_path, + 'csv_use_spc': csv_use_spc, + 'csv_column_spcx': csv_column_spcx, + 'csv_column_spcy': csv_column_spcy, + 'csv_column_spcz': csv_column_spcz, + 'ref': ref, + 'size': size, + 'img_spacing': img_spacing, + 'spacing': spacing, + 'origin': origin, + 'linear': linear, + 'center': center, + 'fit_spacing': fit_spacing, + 'iso_spacing': iso_spacing, + 'image_dimension': image_dimension, + 'pixel_dimension': pixel_dimension, + 'rgb': rgb, + 'ow': ow, + 'out': out, + 'out_ext': out_ext, + } + print("args : ",args) + resample_images(args) def transform_size(size_str): """ @@ -112,30 +61,26 @@ def transform_size(size_str): return size_transformed -def main(args): - csv_path = create_csv(args.input_folder,args.output_folder,output_csv=args.output_folder,name_csv="resample_csv.csv") +def main(input_folder,output_folder,resample_size,spacing,iso_spacing): + csv_path = create_csv(input_folder,output_folder,output_csv=output_folder,name_csv="resample_csv.csv") with open(csv_path, mode='r') as csv_file: csv_reader = csv.DictReader(csv_file) - - # Boucle sur chaque ligne du fichier CSV for row in csv_reader: - # Afficher les informations de chaque ligne - size = tuple(map(int, row["size"].strip("()").split(","))) + size_file = tuple(map(int, row["size"].strip("()").split(","))) input_path = row["in"] out_path = row["out"] - print(size[1]) - print(f'Image d\'entrée: {row["in"]}, Image de sortie: {row["out"]}, Taille: {size}') - # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 768 576 768 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 119 --spacing 0.3 0.3 0.3 --center False --linear False --fit_spacing True --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - # command = [f"python3 {args.python_file} --img \"{input_path}\" --out \"{out_path}\" --size 443 443 443 --fit_spacing True --center 0 --iso_spacing 1 --linear False --image_dimension 3 --pixel_dimension 1 --rgb False --ow 0"] - args_resample = create_args(img=input_path,out=out_path,size=args.resample_size,fit_spacing=True,center=0,iso_spacing=1,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) - print("args resample : ",args_resample) - break - # subprocess.run(command,shell=True) - resample_images(args_resample) + if resample_size != "None" and spacing=="None" : + print("1"*100) + run_resample(img=input_path,out=out_path,size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=iso_spacing,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + elif resample_size == "None" and spacing!="None" : + print("2"*100) + run_resample(img=input_path,out=out_path,img_spacing=list(map(float, spacing.split(','))),size=[size_file[0],size_file[1],size_file[2]],fit_spacing=True,center=0,iso_spacing=True,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + elif resample_size != "None" and spacing!="None" : + print("3"*100) + run_resample(img=input_path,out=out_path,img_spacing=list(map(float, spacing.split(','))),size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=True,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) - delete_csv(csv_path) + # delete_csv(csv_path) def delete_csv(file_path): """Delete a CSV file if it exists.""" @@ -152,13 +97,16 @@ def delete_csv(file_path): if __name__=="__main__": - # SIZE AND SPACING TO RESAMPLE ARE HARD WRITTEN IN THE LINE 24 parser = argparse.ArgumentParser(description='Get nifti info') - parser.add_argument('input_folder', type=str, help='Input path') + parser.add_argument('input_folder_MRI', type=str, help='Input path') + parser.add_argument('input_folder_CBCT', type=str, help='Input path') parser.add_argument('output_folder', type=str, help='Output path') parser.add_argument('resample_size', type=str, help='size_resample') - # /home/luciacev/Documents/Gaelle/MultimodelRegistration/resample/resample.py + parser.add_argument('spacing', type=str, help='size_resample') args = parser.parse_args() - main(args) \ No newline at end of file + if os.path.isdir(args.input_folder_MRI): + main(args.input_folder_MRI,args.output_folder,args.resample_size,args.spacing,iso_spacing=True) + if os.path.isdir(args.input_folder_CBCT): + main(args.input_folder_CBCT,args.output_folder,args.resample_size,args.spacing,iso_spacing=False) \ No newline at end of file diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml index 5729369..e4a083b 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.xml @@ -16,25 +16,40 @@ - input_folder - + input_folder_MRI + 0 - Path for the input. + Path for the input MRI. + + + + input_folder_CBCT + + 1 + Path for the input CBCT. output_folder - 1 + 2 output folder resample_size - 2 + 3 size + + spacing + + 4 + spacing + + + From 04ea2eae4bc1f331d86287674cd9d6f3d32727e0 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Thu, 18 Jul 2024 18:19:38 -0400 Subject: [PATCH 6/9] ENH : connect normalization --- MRI2CBCT_CLI/CMakeLists.txt | 1 + MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI.py | 194 ++++++++++++++++++ .../MRI2CBCT_CLI_utils/AREG_MRI_folder.py | 161 --------------- MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py | 5 + .../{apply_mask_folder.py => apply_mask.py} | 61 ++++-- .../MRI2CBCT_CLI_utils/mri_inverse.py | 9 + .../normalize_percentile.py | 36 +++- MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py | 174 +++++++++------- .../MRI2CBCT_CLI_utils/resample_create_csv.py | 18 ++ MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py | 185 +++++++++-------- 10 files changed, 501 insertions(+), 343 deletions(-) create mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI.py delete mode 100644 MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py rename MRI2CBCT_CLI/MRI2CBCT_CLI_utils/{apply_mask_folder.py => apply_mask.py} (67%) diff --git a/MRI2CBCT_CLI/CMakeLists.txt b/MRI2CBCT_CLI/CMakeLists.txt index d0acb86..9a74ba1 100644 --- a/MRI2CBCT_CLI/CMakeLists.txt +++ b/MRI2CBCT_CLI/CMakeLists.txt @@ -1,6 +1,7 @@ #----------------------------------------------------------------------------- add_subdirectory(MRI2CBCT_ORIENT_CENTER_MRI) add_subdirectory(MRI2CBCT_RESAMPLE_CBCT_MRI) +add_subdirectory(MRI2CBCT_REG) # add_subdirectory(MRI2CBCT_CLI_utils) # add_subdirectory(MRI2CBCT_RESAMPLE_CBCT_MRI) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI.py new file mode 100644 index 0000000..29be884 --- /dev/null +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI.py @@ -0,0 +1,194 @@ +import argparse +import os +import itk +import SimpleITK as sitk +import numpy as np + +def ComputeFinalMatrix(Transforms): + """Compute the final matrix from the list of matrices and translations""" + Rotation, Translation = [], [] + for i in range(len(Transforms)): + Rotation.append(Transforms[i].GetMatrix()) + Translation.append(Transforms[i].GetTranslation()) + + # Compute the final rotation matrix + final_rotation = np.reshape(np.asarray(Rotation[0]), (3, 3)) + for i in range(1, len(Rotation)): + final_rotation = final_rotation @ np.reshape(np.asarray(Rotation[i]), (3, 3)) + + # Compute the final translation matrix + final_translation = np.reshape(np.asarray(Translation[0]), (1, 3)) + for i in range(1, len(Translation)): + final_translation = final_translation + np.reshape( + np.asarray(Translation[i]), (1, 3) + ) + + # Create the final transform + final_transform = sitk.Euler3DTransform() + final_transform.SetMatrix(final_rotation.flatten().tolist()) + final_transform.SetTranslation(final_translation[0].tolist()) + + return final_transform + + +def ElastixReg(fixed_image, moving_image, initial_transform=None): + """Perform a registration using elastix with a rigid transform and possibly an initial transform""" + + elastix_object = itk.ElastixRegistrationMethod.New(fixed_image, moving_image) + + # ParameterMap + parameter_object = itk.ParameterObject.New() + default_rigid_parameter_map = parameter_object.GetDefaultParameterMap("rigid") + parameter_object.AddParameterMap(default_rigid_parameter_map) + parameter_object.SetParameter("ErodeMask", "true") + parameter_object.SetParameter("WriteResultImage", "false") + parameter_object.SetParameter("MaximumNumberOfIterations", "10000") + parameter_object.SetParameter("NumberOfResolutions", "1") + parameter_object.SetParameter("NumberOfSpatialSamples", "10000") + + elastix_object.SetParameterObject(parameter_object) + if initial_transform is not None: + elastix_object.SetInitialTransformParameterObject(initial_transform) + + # Additional parameters + elastix_object.SetLogToConsole(False) + + # Execute registration + elastix_object.UpdateLargestPossibleRegion() + + TransParamObj = elastix_object.GetTransformParameterObject() + + return TransParamObj + +def MatrixRetrieval(TransformParameterMapObject): + """Retrieve the matrix from the transform parameter map""" + ParameterMap = TransformParameterMapObject.GetParameterMap(0) + + if ParameterMap["Transform"][0] == "AffineTransform": + matrix = [float(i) for i in ParameterMap["TransformParameters"]] + # Convert to a sitk transform + transform = sitk.AffineTransform(3) + transform.SetParameters(matrix) + + elif ParameterMap["Transform"][0] == "EulerTransform": + A = [float(i) for i in ParameterMap["TransformParameters"][0:3]] + B = [float(i) for i in ParameterMap["TransformParameters"][3:6]] + # Convert to a sitk transform + transform = sitk.Euler3DTransform() + transform.SetRotation(angleX=A[0], angleY=A[1], angleZ=A[2]) + transform.SetTranslation(B) + + return transform + +def get_corresponding_file(folder, patient_id, modality): + """Get the corresponding file for a given patient ID and modality.""" + for root, _, files in os.walk(folder): + for file in files: + if file.startswith(patient_id) and modality in file and file.endswith(".nii.gz"): + return os.path.join(root, file) + return None + +def registration(cbct_folder,mri_folder,cbct_mask_folder,output_folder,mri_original_folder): + """ + Registers CBCT and MRI images using CBCT masks, saving the results in the specified output folder. + + Arguments: + cbct_folder (str): Folder containing CBCT files (.nii.gz). + mri_folder (str): Folder containing corresponding MRI files (.nii.gz). + cbct_mask_folder (str): Folder containing CBCT masks (.nii.gz). + output_folder (str): Folder to save the registration results. + mri_original_folder (str): Folder containing original MRI files (.nii.gz), if available. + + For each CBCT file in cbct_folder: + - Extract patient ID from the filename. + - Find corresponding MRI and CBCT mask files. + - Optionally, find the original MRI file. + - Call process_images to perform registration and save the results. + """ + + for cbct_file in os.listdir(cbct_folder): + if cbct_file.endswith(".nii.gz") and "_CBCT_" in cbct_file: + patient_id = cbct_file.split("_CBCT_")[0] + + mri_path = get_corresponding_file(mri_folder, patient_id, "_MR_") + if mri_original_folder!="None": + mri_path_original = get_corresponding_file(mri_original_folder, patient_id, "_MR_") + + + cbct_mask_path = get_corresponding_file(cbct_mask_folder, patient_id, "_CBCT_") + + process_images(mri_path, cbct_mask_path, output_folder,patient_id,mri_path_original,) + +def process_images(mri_path, cbct_mask_path, output_folder, patient_id,mri_path_original): + """ + Processes MRI and CBCT mask images, performs registration, and saves the results. + + Arguments: + mri_path (str): Path to the MRI file. + cbct_mask_path (str): Path to the CBCT mask file. + output_folder (str): Folder to save the registration results. + patient_id (str): Identifier for the patient. + mri_path_original (str): Path to the original MRI file. + + Steps: + - Reads the MRI and CBCT mask images. + - Performs registration using Elastix. + - Retrieves the transformation matrix and computes the final transform. + - Saves the transformed image and transformation matrix in the output folder. + """ + + try : + mri_path = itk.imread(mri_path, itk.F) + cbct_mask_path = itk.imread(cbct_mask_path, itk.F) + except KeyError as e: + print("An error occurred while reading the images of the patient : {patient_id}") + print(e) + print(f"{patient_id} failed") + return + + Transforms = [] + + try : + TransformObj_Fine = ElastixReg(cbct_mask_path, mri_path, initial_transform=None) + except Exception as e: + print("An error occurred during the registration process on the patient {patient_id} :") + print(e) + return + + transforms_Fine = MatrixRetrieval(TransformObj_Fine) + Transforms.append(transforms_Fine) + transform = ComputeFinalMatrix(Transforms) + + os.makedirs(output_folder, exist_ok=True) + + output_image_path = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg.nii.gz')) + output_image_path_transform = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg_transform.tfm')) + + sitk.WriteTransform(transform, output_image_path_transform) + + + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description='AREG MRI folder') + + parser.add_argument("--cbct_folder", type=str, help="Folder containing CBCT images.", default=".") + parser.add_argument("--cbct_mask_folder", type=str, help="Folder containing CBCT masks.", default=".") + + parser.add_argument("--mri_folder", type=str, help="Folder containing MRI images.", default=".") + parser.add_argument("--mri_original_folder", type=str, help="Folder containing original MRI.", default=".") + + parser.add_argument("--output_folder", type=str, help="Folder to save the output files.",default=".") + + args = parser.parse_args() + + if not os.path.exists(args.output_folder): + os.makedirs(args.output_folder) + + cbct_folder = args.cbct_folder + mri_folder = args.mri_folder + cbct_mask_folder = args.cbct_mask_folder + output_folder = args.output_folder + mri_original_folder = args.mri_original_folder + + registration(cbct_folder,mri_folder,cbct_mask_folder,output_folder,mri_original_folder) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py deleted file mode 100644 index 0bd78a8..0000000 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/AREG_MRI_folder.py +++ /dev/null @@ -1,161 +0,0 @@ -import argparse -import os -import itk -import SimpleITK as sitk -from AREG_CBCT_utils.utils import ElastixApprox, MatrixRetrieval, ElastixReg, ComputeFinalMatrix, ResampleImage -import numpy as np - -def get_corresponding_file(folder, patient_id, modality): - """Get the corresponding file for a given patient ID and modality.""" - for root, _, files in os.walk(folder): - for file in files: - if file.startswith(patient_id) and modality in file and file.endswith(".nii.gz"): - return os.path.join(root, file) - return None - -def main(args): - cbct_folder = args.cbct_folder - mri_folder = args.mri_folder - cbct_mask_folder = args.cbct_mask_folder - mri_mask_folder = args.mri_mask_folder - cbct_seg_folder = args.cbct_seg_folder - mri_seg_folder = args.mri_seg_folder - output_folder = args.output_folder - mri_original_folder = args.mri_original_folder - cbct_original_folder = args.cbct_original_folder - - cbct_path = "None" - cbct_seg_path = "None" - mri_seg_path = "None" - - for cbct_file in os.listdir(cbct_folder): - if cbct_file.endswith(".nii.gz") and "_CBCT_" in cbct_file: - mri_mask_path="None" - patient_id = cbct_file.split("_CBCT_")[0] - - # cbct_path = os.path.join(cbct_folder, cbct_file) - cbct_path_original = get_corresponding_file(mri_original_folder, patient_id, "_CBCT_") - mri_path = get_corresponding_file(mri_folder, patient_id, "_MR_") - if mri_original_folder!="None": - mri_path_original = get_corresponding_file(mri_original_folder, patient_id, "_MR_") - - # if not mri_path: - # print(f"Corresponding MRI file for {cbct_file} not found.") - # continue - - cbct_mask_path = get_corresponding_file(cbct_mask_folder, patient_id, "_CBCT_") - # if mri_mask_folder!="None" : - # mri_mask_path = get_corresponding_file(mri_mask_folder, patient_id, "_MR_") - - # cbct_seg_path = get_corresponding_file(cbct_seg_folder, patient_id, "_CBCT_") - mri_seg_path = get_corresponding_file(mri_seg_folder, patient_id, "_MR_") - - # if not all([cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path]): - # print(f"One or more corresponding mask or segmentation files for {cbct_file} not found.") - # continue - print("-"*50) - # print("cbct_mask_path : ",cbct_mask_path) - print("mri_path : ",mri_path) - print("mri_path_original : ",mri_path_original) - print("mri_seg_path : ",mri_seg_path) - print("cbct_mask_path : ",cbct_mask_path) - # print("mri_path : ",mri_path) - - process_images(cbct_path, mri_path, cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path, output_folder,patient_id,mri_path_original,cbct_path_original) - -def process_images(cbct_path, mri_path, cbct_mask_path, mri_mask_path, cbct_seg_path, mri_seg_path, output_folder, patient_id,mri_path_original,cbct_path_original): - - - # cbct_path = itk.imread(cbct_path, itk.F) - try : - mri_path = itk.imread(mri_path, itk.F) - cbct_mask_path = itk.imread(cbct_mask_path, itk.F) - except KeyError as e: - print("An error occurred while reading the images") - print(e) - print(f"{patient_id} failed") - return - - Transforms = [] - - # TransformObj_Approx = np.eye(4) # 4x4 identity matrix - try : - TransformObj_Fine = ElastixReg(cbct_mask_path, mri_path, initial_transform=None) - except Exception as e: - print("An error occurred during the registration process:") - print(e) - print(f"{patient_id} failed") - return - - print(f"{patient_id} a ete traite") - transforms_Fine = MatrixRetrieval(TransformObj_Fine) - Transforms.append(transforms_Fine) - transform = ComputeFinalMatrix(Transforms) - - os.makedirs(output_folder, exist_ok=True) - - output_image_path = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg.nii.gz')) - output_image_path_transform = os.path.join(output_folder,os.path.basename(mri_path_original).replace('.nii.gz', f'_reg_transform.tfm')) - - sitk.WriteTransform(transform, output_image_path_transform) - - # resample_t2 = sitk.Cast(ResampleImage(sitk.ReadImage(mri_path_original), transform), sitk.sitkInt16) - # sitk.WriteImage(resample_t2, output_image_path) - - - # if mri_seg_path!="None": - # output_image_path_seg = os.path.join(output_folder,os.path.basename(mri_seg_path).replace('.nii.gz', f'_reg.nii.gz')) - # try : - # resample_seg = sitk.Cast(ResampleImage(sitk.ReadImage(mri_seg_path), transform), sitk.sitkInt16) - # sitk.WriteImage(resample_seg, output_image_path_seg) - - # except KeyError as e : - # print("Error to apply the matrix to : ",output_image_path_seg) - # print(e) - - - - - -def print_min_max(image, image_name): - image_array = itk.array_from_image(image) - min_value = image_array.min() - max_value = image_array.max() - print(f"{image_name} - Min: {min_value}, Max: {max_value}") - - - - -if __name__ == "__main__": - parser = argparse.ArgumentParser(description='AREG MRI folder') - - parser.add_argument("--cbct_folder", type=str, help="Folder containing CBCT images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm/test_percentile=[10,95]_norm=[0,75]") - parser.add_argument("--cbct_original_folder", type=str, help="Folder containing original CBCT.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b0_CBCT") - parser.add_argument("--cbct_mask_folder", type=str, help="Folder containing CBCT masks.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b3_CBCT_inv_norm_mask:l2/test_percentile=[10,95]_norm=[0,75]") - parser.add_argument("--cbct_seg_folder", type=str, help="Folder containing CBCT segmentations.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/d0_CBCT_seg_sep/label_2") - - parser.add_argument("--mri_folder", type=str, help="Folder containing MRI images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a2_MRI_inv_norm/test_percentile=[0,100]_norm=[0,100]") - parser.add_argument("--mri_original_folder", type=str, help="Folder containing original MRI.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a0_MRI") - parser.add_argument("--mri_mask_folder", type=str, help="Folder containing MRI masks.", default="None") - parser.add_argument("--mri_seg_folder", type=str, help="Folder containing MRI segmentations.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/c0_MRI_seg_sep/label_2") - - parser.add_argument("--output_folder", type=str, help="Folder to save the output files.",default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/z01_output/a03_mri:inv+norm[0,100]+p[0,100]_cbct:norm[0,75]+p[10,95]+mask") - - # parser.add_argument("--cbct_folder", type=str, help="Folder containing CBCT images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b2_CBCT_norm/test_percentile=[10,95]_norm=[0,75]") - # parser.add_argument("--cbct_original_folder", type=str, help="Folder containing original CBCT.", default="None") - # parser.add_argument("--cbct_mask_folder", type=str, help="Folder containing CBCT masks.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/b3_CBCT_inv_norm_mask:l2/test_percentile=[10,95]_norm=[0,75]") - # parser.add_argument("--cbct_seg_folder", type=str, help="Folder containing CBCT segmentations.", default="None") - - # parser.add_argument("--mri_folder", type=str, help="Folder containing MRI images.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a2_MRI_inv_norm/test_percentile=[0,100]_norm=[0,100]") - # parser.add_argument("--mri_original_folder", type=str, help="Folder containing original MRI.", default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/a0_MRI") - # parser.add_argument("--mri_mask_folder", type=str, help="Folder containing MRI masks.", default="None") - # parser.add_argument("--mri_seg_folder", type=str, help="Folder containing MRI segmentations.", default="None") - - # parser.add_argument("--output_folder", type=str, help="Folder to save the output files.",default="/home/lucia/Documents/Gaelle/Data/MultimodelReg/Segmentation/a3_Registration_closer_all/z01_output/a02_mri:inv+norm[0,100]+p[0,100]_cbct:norm[0,75]+p[10,95]+mask") - - args = parser.parse_args() - - if not os.path.exists(args.output_folder): - os.makedirs(args.output_folder) - - main(args) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py index 1378ee9..683fe84 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/__init__.py @@ -4,3 +4,8 @@ # from .Net import DenseNet from .resample import resample_images, run_resample + +from .mri_inverse import invert_mri_intensity +from .normalize_percentile import normalize +from .apply_mask import apply_mask_f +from .AREG_MRI import registration diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask.py similarity index 67% rename from MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py rename to MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask.py index a8a6dc0..614b425 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask_folder.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/apply_mask.py @@ -5,19 +5,24 @@ def MaskedImage(fixed_image_path, fixed_seg_path, folder_output, suffix, SegLabel=None): - """Mask the fixed image with the fixed segmentation and write it to a file""" - # print("fixed_seg_path : ",fixed_seg_path) - # print("fixed_image_path : ",fixed_image_path) + """ + Mask the fixed image with the fixed segmentation and write it to a file + + Arguments: + fixed_image_path (str): Path to the fixed image file. + fixed_seg_path (str): Path to the fixed segmentation file. + folder_output (str): Folder to save the masked image. + suffix (str): Suffix to add to the output file name. + SegLabel (int, optional): Segmentation label to use for masking. + """ fixed_image_sitk = sitk.ReadImage(fixed_image_path) fixed_seg_sitk = sitk.ReadImage(fixed_seg_path) - # fixed_seg_sitk.SetOrigin(fixed_image_sitk.GetOrigin()) fixed_image_masked = applyMask(fixed_image_sitk, fixed_seg_sitk, label=SegLabel) if fixed_image_masked=="failed": print("failed process on : ",fixed_image_sitk) return - # Write the masked image base_name, ext = os.path.splitext(fixed_image_path) if base_name.endswith('.nii'): # Case for .nii.gz ext = '.nii.gz' @@ -25,17 +30,22 @@ def MaskedImage(fixed_image_path, fixed_seg_path, folder_output, suffix, SegLabe file_name = os.path.basename(fixed_image_path) file_name_without_ext = os.path.splitext(os.path.splitext(file_name)[0])[0] - # Construction du chemin de fichier de sortie output_path = os.path.join(folder_output, f"{file_name_without_ext}_{suffix}{ext}") - print("-"*100) - print("output_path : ",output_path) + sitk.WriteImage(sitk.Cast(fixed_image_masked, sitk.sitkInt16), output_path) return output_path def applyMask(image, mask, label): - """Apply a mask to an image.""" + """ + Apply a mask to an image. + + Arguments: + image (SimpleITK.Image): The image to be masked. + mask (SimpleITK.Image): The mask image. + label (int): The label value to use for masking. + """ try : array = sitk.GetArrayFromImage(mask) if label is not None and label in np.unique(array): @@ -50,21 +60,35 @@ def applyMask(image, mask, label): def find_segmentation_file(image_file, seg_folder): - """Find the corresponding segmentation file for a given image file.""" + """ + Find the corresponding segmentation file for a given image file. + + Arguments: + image_file (str): Path to the image file. + seg_folder (str): Folder containing segmentation files. + """ base_name = os.path.basename(image_file) patient_id = base_name.split('_CBCT')[0].split('_MR')[0] - # print("patient_id : ",patient_id) - # print("base_name : ",base_name) for seg_file in os.listdir(seg_folder): - if seg_file.startswith(patient_id) and seg_file.endswith('_label2.nii.gz'): + if seg_file.startswith(patient_id) and "_CBCT" in seg_file: return os.path.join(seg_folder, seg_file) return None -def process_folder(folder_path, seg_folder, folder_output, suffix, seg_label): - """Process all files in the specified folder.""" +def apply_mask_f(folder_path, seg_folder, folder_output, suffix, seg_label): + """ + Processes all image files in the specified folder by applying the corresponding segmentation masks. + + Arguments: + folder_path (str): Path to the folder containing image files. + seg_folder (str): Folder containing segmentation files. + folder_output (str): Folder to save the masked images. + suffix (str): Suffix to add to the output file names. + seg_label (int): Segmentation label to use for masking. + """ + for root, _, files in os.walk(folder_path): for file in files: if file.endswith(('.nii', '.nii.gz')) and ('_CBCT' in file or '_MR' in file): @@ -72,12 +96,11 @@ def process_folder(folder_path, seg_folder, folder_output, suffix, seg_label): fixed_seg_path = find_segmentation_file(file, seg_folder) if fixed_seg_path: - # print(f"Processing file: {fixed_image_path}") try : MaskedImage(fixed_image_path, fixed_seg_path, folder_output, suffix, seg_label) - # print(f"Segmentation file for {fixed_image_path} succedeed.") + print(f"Mask apply for the file {fixed_image_path} succedeed.") except KeyError as e: - print(f"Segmentation file for {fixed_image_path} failed.") + print(f"Mask apply for the file {fixed_image_path}failed.") print(e) continue else: @@ -98,4 +121,4 @@ def process_folder(folder_path, seg_folder, folder_output, suffix, seg_label): if not os.path.exists(args.folder_output): os.makedirs(args.folder_output) - process_folder(args.folder_path, args.seg_folder, args.folder_output, args.suffix, args.seg_label) + apply_mask_f(args.folder_path, args.seg_folder, args.folder_output, args.suffix, args.seg_label) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py index 86796f3..c61b19c 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/mri_inverse.py @@ -3,6 +3,15 @@ import argparse def invert_mri_intensity(path_folder, folder_output, suffix): + """ + Inverts the intensity values of MRI images in the specified folder and saves the results. + + Arguments: + path_folder (str): Path to the folder containing MRI files. + folder_output (str): Folder to save the inverted images. + suffix (str): Suffix to add to the output file names. + """ + # Check if the output folder exists, if not create it if not os.path.exists(folder_output): os.makedirs(folder_output) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py index 0780843..bc01d46 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py @@ -4,14 +4,30 @@ import numpy as np def compute_thresholds(image, lower_percentile=10, upper_percentile=90): - """Compute thresholds based on percentiles of the image histogram.""" + """ + Computes intensity thresholds for an image based on specified percentiles. + + Arguments: + image (SimpleITK.Image): The input image. + lower_percentile (float): The lower percentile for threshold computation (default is 10). + upper_percentile (float): The upper percentile for threshold computation (default is 90). + """ array = sitk.GetArrayFromImage(image) lower_threshold = np.percentile(array, lower_percentile) upper_threshold = np.percentile(array, upper_percentile) return lower_threshold, upper_threshold def enhance_contrast(image,upper_percentile,lower_percentile, min_norm, max_norm): - """Enhance the contrast of the image while keeping normalization between 0 and 1.""" + """ + Enhances the contrast of the image while normalizing its intensity values. + + Arguments: + image (SimpleITK.Image): The input image. + upper_percentile (float): The upper percentile for threshold computation. + lower_percentile (float): The lower percentile for threshold computation. + min_norm (float): The minimum normalization value. + max_norm (float): The maximum normalization value. + """ # Compute thresholds lower_threshold, upper_threshold = compute_thresholds(image,lower_percentile,upper_percentile) # print(f"Computed thresholds - Lower: {lower_threshold}, Upper: {upper_threshold}") @@ -24,8 +40,18 @@ def enhance_contrast(image,upper_percentile,lower_percentile, min_norm, max_norm return sitk.GetImageFromArray(scaled_array) -def process_images(input_folder, output_folder,upper_percentile,lower_percentile,min_norm, max_norm): - """Process all .nii.gz images in the input folder.""" +def normalize(input_folder, output_folder,upper_percentile,lower_percentile,min_norm, max_norm): + """ + Processes and normalizes all .nii.gz images in the input folder, enhancing their contrast. + + Arguments: + input_folder (str): Path to the folder containing the input images. + output_folder (str): Path to the folder to save the normalized images. + upper_percentile (float): Upper percentile for threshold computation. + lower_percentile (float): Lower percentile for threshold computation. + min_norm (float): Minimum normalization value. + max_norm (float): Maximum normalization value. + """ if not os.path.exists(output_folder): os.makedirs(output_folder) @@ -62,7 +88,7 @@ def main(): if not os.path.exists(output_path): os.makedirs(output_path) - process_images(args.input_folder, output_path, args.upper_percentile,args.lower_percentile,args.min_norm, args.max_norm) + normalize(args.input_folder, output_path, args.upper_percentile,args.lower_percentile,args.min_norm, args.max_norm) if __name__ == '__main__': main() diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py index 0112f99..cef2d38 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py @@ -7,6 +7,21 @@ import csv def resample_fn(img, args): + ''' + Resamples the given image based on the specified arguments. + + Arguments: + img (SimpleITK.Image): The image to be resampled. + args (dict): Dictionary containing the following keys: + - size (tuple): Desired size of the output image. + - fit_spacing (bool): Flag to fit spacing. + - iso_spacing (bool): Flag for isotropic spacing. + - pixel_dimension (int): Pixel dimension of the image. + - center (int): Flag to center the image. + - linear (bool): Flag to use linear interpolation. + - spacing (tuple): Desired spacing of the output image (optional). + - origin (tuple): Desired origin of the output image (optional). + ''' output_size = args['size'] fit_spacing = args['fit_spacing'] iso_spacing = args['iso_spacing'] @@ -71,12 +86,25 @@ def resample_fn(img, args): def Resample(img_filename, args): - - output_size = args['size'] - fit_spacing = args['fit_spacing'] - iso_spacing = args['iso_spacing'] - img_dimension = args['image_dimension'] - pixel_dimension = args['pixel_dimension'] + """ + Resamples an image based on the provided arguments. + + Arguments: + img_filename (str): Path to the image file to resample. + args (dict): Dictionary containing the following keys: + - size (tuple): Desired size of the output image. + - fit_spacing (bool): Flag to fit spacing. + - iso_spacing (bool): Flag for isotropic spacing. + - image_dimension (int): Dimension of the image. + - pixel_dimension (int): Pixel dimension of the image. + - img_spacing (tuple): Spacing of the input image. + + Steps: + - Reads the image from the specified file. + - Sets the image spacing if provided in the arguments. + - Calls the resample function with the image and arguments. + - Returns the resampled image. + """ print("Reading:", img_filename) img = sitk.ReadImage(img_filename) @@ -88,6 +116,10 @@ def Resample(img_filename, args): def resample_images(args): + """ + Resamples images based on the provided arguments and saves the output. + """ + filenames = [] if args['img']: fobj = {"img": args['img'], "out": args['out']} @@ -171,79 +203,79 @@ def resample_images(args): except Exception as e: print(e, file=sys.stderr) -# def main(): -# parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) - -# in_group = parser.add_mutually_exclusive_group(required=True) -# in_group.add_argument('--img', type=str, help='image to resample') -# in_group.add_argument('--dir', type=str, help='Directory with image to resample') -# in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') - -# csv_group = parser.add_argument_group('CSV extra parameters') -# csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') -# csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') -# csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') -# csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') -# csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') -# csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') - -# transform_group = parser.add_argument_group('Transform parameters') -# transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) -# transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) -# transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') -# transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') -# transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') -# transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) -# transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) -# transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) -# transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) - -# img_group = parser.add_argument_group('Image parameters') -# img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) -# img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) -# img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) - -# out_group = parser.add_argument_group('Output parameters') -# out_group.add_argument('--ow', type=int, help='Overwrite', default=1) -# out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") -# out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) - -# args = parser.parse_args() -# resample_images(args) - def run_resample(img=None, dir=None, csv=None, csv_column='image', csv_root_path=None, csv_use_spc=0, csv_column_spcx=None, csv_column_spcy=None, csv_column_spcz=None, ref=None, size=None, img_spacing=None, spacing=None, origin=None, linear=False, center=0, fit_spacing=False, iso_spacing=False, image_dimension=2, pixel_dimension=1, rgb=False, ow=1, out="./out.nrrd", out_ext=None): + ''' + Sets up and runs the resampling of images based on the provided parameters. + ''' args = { - 'img': img, - 'dir': dir, - 'csv': csv, - 'csv_column': csv_column, - 'csv_root_path': csv_root_path, - 'csv_use_spc': csv_use_spc, - 'csv_column_spcx': csv_column_spcx, - 'csv_column_spcy': csv_column_spcy, - 'csv_column_spcz': csv_column_spcz, - 'ref': ref, - 'size': size, - 'img_spacing': img_spacing, - 'spacing': spacing, - 'origin': origin, - 'linear': linear, - 'center': center, - 'fit_spacing': fit_spacing, - 'iso_spacing': iso_spacing, - 'image_dimension': image_dimension, - 'pixel_dimension': pixel_dimension, - 'rgb': rgb, - 'ow': ow, - 'out': out, - 'out_ext': out_ext, + 'img': img, # Path to a single image file to resample + 'dir': dir, # Directory containing image files to resample + 'csv': csv, # Path to a CSV file listing images to resample + 'csv_column': csv_column, # CSV column name that contains image paths + 'csv_root_path': csv_root_path, # Root path to prepend to CSV image paths + 'csv_use_spc': csv_use_spc, # Flag to use spacing from CSV + 'csv_column_spcx': csv_column_spcx, # CSV column name for X spacing + 'csv_column_spcy': csv_column_spcy, # CSV column name for Y spacing + 'csv_column_spcz': csv_column_spcz, # CSV column name for Z spacing + 'ref': ref, # Reference image path for resampling + 'size': size, # Desired size of the output image + 'img_spacing': img_spacing, # Spacing of the input image + 'spacing': spacing, # Desired spacing of the output image + 'origin': origin, # Origin of the output image + 'linear': linear, # Flag to use linear interpolation + 'center': center, # Flag to center the image + 'fit_spacing': fit_spacing, # Flag to fit spacing + 'iso_spacing': iso_spacing, # Flag for isotropic spacing + 'image_dimension': image_dimension, # Dimension of the image + 'pixel_dimension': pixel_dimension, # Pixel dimension of the image + 'rgb': rgb, # Flag for RGB images + 'ow': ow, # Overwrite flag + 'out': out, # Output file path + 'out_ext': out_ext, # Output file extension } resample_images(args) if __name__ == "__main__": - run_resample() \ No newline at end of file + parser = argparse.ArgumentParser(description='Resample an image', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + in_group = parser.add_mutually_exclusive_group(required=True) + in_group.add_argument('--img', type=str, help='image to resample') + in_group.add_argument('--dir', type=str, help='Directory with image to resample') + in_group.add_argument('--csv', type=str, help='CSV file with column img with paths to images to resample') + + csv_group = parser.add_argument_group('CSV extra parameters') + csv_group.add_argument('--csv_column', type=str, default='image', help='CSV column name (Only used if flag csv is used)') + csv_group.add_argument('--csv_root_path', type=str, default=None, help='Replaces a root path directory to empty, this is use to recreate a directory structure in the output directory, otherwise, the output name will be the name in the csv (only if csv flag is used)') + csv_group.add_argument('--csv_use_spc', type=int, default=0, help='Use the spacing information in the csv instead of the image') + csv_group.add_argument('--csv_column_spcx', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcy', type=str, default=None, help='Column name in csv') + csv_group.add_argument('--csv_column_spcz', type=str, default=None, help='Column name in csv') + + transform_group = parser.add_argument_group('Transform parameters') + transform_group.add_argument('--ref', type=str, help='Reference image. Use an image as reference for the resampling', default=None) + transform_group.add_argument('--size', nargs="+", type=int, help='Output size, -1 to leave unchanged', default=None) + transform_group.add_argument('--img_spacing', nargs="+", type=float, default=None, help='Use this spacing information instead of the one in the image') + transform_group.add_argument('--spacing', nargs="+", type=float, default=None, help='Output spacing') + transform_group.add_argument('--origin', nargs="+", type=float, default=None, help='Output origin') + transform_group.add_argument('--linear', type=bool, help='Use linear interpolation.', default=False) + transform_group.add_argument('--center', type=int, help='Center the image in the space', default=0) + transform_group.add_argument('--fit_spacing', type=bool, help='Fit spacing to output', default=False) + transform_group.add_argument('--iso_spacing', type=bool, help='Same spacing for resampled output', default=False) + + img_group = parser.add_argument_group('Image parameters') + img_group.add_argument('--image_dimension', type=int, help='Image dimension', default=2) + img_group.add_argument('--pixel_dimension', type=int, help='Pixel dimension', default=1) + img_group.add_argument('--rgb', type=bool, help='Use RGB type pixel', default=False) + + out_group = parser.add_argument_group('Output parameters') + out_group.add_argument('--ow', type=int, help='Overwrite', default=1) + out_group.add_argument('--out', type=str, help='Output image/directory', default="./out.nrrd") + out_group.add_argument('--out_ext', type=str, help='Output extension type', default=None) + + args = parser.parse_args() + resample_images(args) diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py index 2c63fa8..79af099 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample_create_csv.py @@ -4,6 +4,14 @@ import argparse def get_nifti_info(file_path,output_resample): + """ + Retrieves information about a nifti file and prepares the output path for resampling. + + Arguments: + file_path (str): Path to the input nifti file. + output_resample (str): Path to the folder to save resampled nifti files. + """ + # Read the NIfTI file image = sitk.ReadImage(file_path) @@ -18,6 +26,16 @@ def get_nifti_info(file_path,output_resample): return info def create_csv(input:str,output_resample:str,output_csv:str,name_csv:str): + """ + Creates a CSV file with information about nifti files in the input folder, resampling them if needed. + + Arguments: + input (str): Path to the input folder containing nifti files. + output_resample (str): Path to the folder to save resampled nifti files. + output_csv (str): Path to the folder to save the output CSV file. + name_csv (str): Name of the output CSV file. + """ + if not os.path.exists(output_resample): os.makedirs(output_resample) diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py index 679d181..71c7dd9 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py @@ -5,94 +5,123 @@ import os import re + +import sys +fpath = os.path.join(os.path.dirname(__file__), "..") +sys.path.append(fpath) +from MRI2CBCT_CLI_utils import invert_mri_intensity, normalize, apply_mask_f, registration + def create_folder(folder): + """ + Creates a folder if it does not already exist. + + Arguments: + folder (str): Path of the folder to create. + """ if not os.path.exists(folder): os.makedirs(folder) -def run_script(script_name, args): - command = ['python', script_name] + args - result = subprocess.run(command, capture_output=True, text=True) - print(f"Running {script_name} with arguments {args}") - print("Output:\n", result.stdout) - if result.stderr: - print("Errors:\n", result.stderr) def run_script_inverse_mri(mri_folder, folder_general): + """ + Inverts the intensity of MRI images and saves the results. + + Arguments: + mri_folder (str): Folder containing MRI files. + folder_general (str): General folder for output. + """ + folder_mri_inverse = os.path.join(folder_general,"a01_MRI_inv") create_folder(folder_mri_inverse) - script_name = 'mri_inverse.py' - args = [ - f"--path_folder={mri_folder}", - f"--folder_output={folder_mri_inverse}", - f"--suffix=inv", - ] - run_script(script_name, args) + invert_mri_intensity(mri_folder, folder_mri_inverse, "inv") return folder_mri_inverse def run_script_normalize_percentile(file_type,input_folder, folder_general, upper_percentile, lower_percentile, max_norm, min_norm): - script_name = 'normalize_percentile.py' + """ + Normalizes images based on specified percentiles and saves the results. + + Arguments: + file_type (str): Type of files to normalize ('MRI' or other). + input_folder (str): Folder containing the input files. + folder_general (str): General folder for output. + upper_percentile (float): Upper percentile for normalization. + lower_percentile (float): Lower percentile for normalization. + max_norm (float): Maximum value for normalization. + min_norm (float): Minimum value for normalization. + """ + if file_type=="MRI": - output_folder_norm = os.path.join(folder_general,"a2_MRI_inv_norm") + output_folder_norm_general = os.path.join(folder_general,"a2_MRI_inv_norm") else : - output_folder_norm = os.path.join(folder_general,"b2_CBCT_norm") + output_folder_norm_general = os.path.join(folder_general,"b2_CBCT_norm") + create_folder(output_folder_norm_general) + + output_folder_norm = os.path.join(output_folder_norm_general,f"percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") create_folder(output_folder_norm) - args = [ - f"--input_folder={input_folder}", - f"--output_folder={output_folder_norm}", - f"--upper_percentile={upper_percentile}", - f"--lower_percentile={lower_percentile}", - f"--max_norm={max_norm}", - f"--min_norm={min_norm}" - ] - run_script(script_name, args) - output_path_norm = os.path.join(output_folder_norm,f"test_percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") - return output_path_norm + + normalize(input_folder, output_folder_norm,upper_percentile,lower_percentile,min_norm, max_norm) + return output_folder_norm def run_script_apply_mask(cbct_folder, cbct_label2,folder_general, suffix,upper_percentile, lower_percentile, max_norm, min_norm): - script_name = 'apply_mask_folder.py' - cbct_mask_folder = os.path.join(folder_general,"b3_CBCT_norm_mask:l2",f"test_percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") + """ + Applies a mask to CBCT images and saves the normalized results. + + Arguments: + cbct_folder (str): Folder containing CBCT files. + cbct_label2 (str): Folder containing the segmentation labels. + folder_general (str): General folder for output. + suffix (str): Suffix for the output files. + upper_percentile (float): Upper percentile for normalization. + lower_percentile (float): Lower percentile for normalization. + max_norm (float): Maximum value for normalization. + min_norm (float): Minimum value for normalization. + """ + cbct_mask_folder = os.path.join(folder_general,"b3_CBCT_norm_mask:l2",f"percentile=[{lower_percentile},{upper_percentile}]_norm=[{min_norm},{max_norm}]") create_folder(cbct_mask_folder) - args = [ - f"--folder_path={cbct_folder}", - f"--seg_folder={cbct_label2}", - f"--folder_output={cbct_mask_folder}", - f"--suffix={suffix}", - f"--seg_label={1}" - ] - run_script(script_name, args) + apply_mask_f(folder_path=cbct_folder, seg_folder=cbct_label2, folder_output=cbct_mask_folder, suffix=suffix, seg_label=1) return cbct_mask_folder def run_script_AREG_MRI_folder(cbct_folder, cbct_mask_folder,mri_folder,mri_original_folder,folder_general,mri_lower_p,mri_upper_p,mri_min_norm,mri_max_norm,cbct_lower_p,cbct_upper_p,cbct_min_norm,cbct_max_norm): - script_name = 'AREG_MRI_folder.py' + """ + Runs the registration script for MRI and CBCT folders, applying normalization and percentile adjustments. + + Arguments: + cbct_folder (str): Folder containing CBCT files. + cbct_mask_folder (str): Folder containing CBCT mask files. + mri_folder (str): Folder containing MRI files. + mri_original_folder (str): Folder containing original MRI files. + folder_general (str): General folder for output. + mri_lower_p (float): Lower percentile for MRI normalization. + mri_upper_p (float): Upper percentile for MRI normalization. + mri_min_norm (float): Minimum value for MRI normalization. + mri_max_norm (float): Maximum value for MRI normalization. + cbct_lower_p (float): Lower percentile for CBCT normalization. + cbct_upper_p (float): Upper percentile for CBCT normalization. + cbct_min_norm (float): Minimum value for CBCT normalization. + cbct_max_norm (float): Maximum value for CBCT normalization. + """ + output_folder = os.path.join(folder_general,"z01_output",f"a01_mri:inv+norm[{mri_min_norm},{mri_max_norm}]+p[{mri_lower_p},{mri_upper_p}]_cbct:norm[{cbct_min_norm},{cbct_max_norm}]+p[{cbct_lower_p},{cbct_upper_p}]+mask") create_folder(output_folder) - args = [ - f"--cbct_folder={cbct_folder}", - f"--cbct_original_folder=None", - f"--cbct_mask_folder={cbct_mask_folder}", - f"--cbct_seg_folder=None", - f"--mri_folder={mri_folder}", - f"--mri_original_folder={mri_original_folder}", - f"--mri_mask_folder=None", - f"--mri_seg_folder=None", - f"--output_folder={output_folder}" - ] - run_script(script_name, args) + registration(cbct_folder,mri_folder,cbct_mask_folder,output_folder,mri_original_folder) return cbct_mask_folder def extract_values(input_string): - # Utiliser une expression régulière pour extraire tous les chiffres - numbers = re.findall(r'\d+', input_string) + """ + Extracts 8 integers from the input string and returns them as a tuple. + + Arguments: + input_string (str): String containing the integers. + """ - # Convertir les nombres extraits en entiers + numbers = re.findall(r'\d+', input_string) + numbers = list(map(int, numbers)) - # Vérifier qu'il y a exactement 8 nombres if len(numbers) != 8: - raise ValueError("L'entrée doit contenir exactement 8 chiffres.") + raise ValueError("The input need to contains 8 numbers") - # Assigner les nombres à des variables séparées a, b, c, d, e, f, g, h = numbers return a, b, c, d, e, f, g, h @@ -105,40 +134,22 @@ def main(): parser.add_argument('cbct_label2', type=str, help="Folder containing CBCT masks.") parser.add_argument('normalization', type=str, help="Folder containing CBCT masks.") args = parser.parse_args() - print("normalization : ",args.normalization) - print("type normalization : ",type(args.normalization)) mri_min_norm, mri_max_norm, mri_lower_p, mri_upper_p, cbct_min_norm, cbct_max_norm, cbct_lower_p, cbct_upper_p = extract_values(args.normalization) - print(f"mri_lower_p: {mri_lower_p}") - print(f"mri_upper_p: {mri_upper_p}") - print(f"mri_min_norm: {mri_min_norm}") - print(f"mri_max_norm: {mri_max_norm}") - print(f"cbct_lower_p: {cbct_lower_p}") - print(f"cbct_upper_p: {cbct_upper_p}") - print(f"cbct_min_norm: {cbct_min_norm}") - print(f"cbct_max_norm: {cbct_max_norm}") - - # Appel des fonctions une par une - # mri_lower_p = 10 - # mri_upper_p = 95 - # mri_min_norm = 0 - # mri_max_norm = 100 - - # cbct_lower_p = 10 - # cbct_upper_p = 95 - # cbct_min_norm = 0 - # cbct_max_norm = 150 - # # MRI - # folder_mri_inverse = run_script_inverse_mri(args.mri_folder, args.folder_general) - # input_path_norm_mri = run_script_normalize_percentile("MRI",folder_mri_inverse, args.folder_general, upper_percentile=mri_upper_p, lower_percentile=mri_lower_p, max_norm=mri_max_norm, min_norm=mri_min_norm) - - # # CBCT - # output_path_norm_cbct = run_script_normalize_percentile("CBCT",args.cbct_folder, args.folder_general, upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) - # input_path_cbct_norm_mask = run_script_apply_mask(output_path_norm_cbct,args.cbct_label2,args.folder_general,"mask",upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) + # MRI + folder_mri_inverse = run_script_inverse_mri(args.mri_folder, args.folder_general) + input_path_norm_mri = run_script_normalize_percentile("MRI",folder_mri_inverse, args.folder_general, upper_percentile=mri_upper_p, lower_percentile=mri_lower_p, max_norm=mri_max_norm, min_norm=mri_min_norm) + + # CBCT + output_path_norm_cbct = run_script_normalize_percentile("CBCT",args.cbct_folder, args.folder_general, upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) + input_path_cbct_norm_mask = run_script_apply_mask(output_path_norm_cbct,args.cbct_label2,args.folder_general,"mask",upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) + print('input_path_cbct_norm_mask : ',input_path_cbct_norm_mask) - # # REG - # run_script_AREG_MRI_folder(cbct_folder=args.cbct_folder,cbct_mask_folder=input_path_cbct_norm_mask,mri_folder=input_path_norm_mri,mri_original_folder=args.mri_folder,folder_general=args.folder_general,mri_lower_p=mri_lower_p,mri_upper_p=mri_upper_p,mri_min_norm=mri_min_norm,mri_max_norm=mri_max_norm,cbct_lower_p=cbct_lower_p,cbct_upper_p=cbct_upper_p,cbct_min_norm=cbct_min_norm,cbct_max_norm=cbct_max_norm) + # REG + print("*"*100) + run_script_AREG_MRI_folder(cbct_folder=args.cbct_folder,cbct_mask_folder=input_path_cbct_norm_mask,mri_folder=input_path_norm_mri,mri_original_folder=args.mri_folder,folder_general=args.folder_general,mri_lower_p=mri_lower_p,mri_upper_p=mri_upper_p,mri_min_norm=mri_min_norm,mri_max_norm=mri_max_norm,cbct_lower_p=cbct_lower_p,cbct_upper_p=cbct_upper_p,cbct_min_norm=cbct_min_norm,cbct_max_norm=cbct_max_norm) + print("EENNNDDD") From 01bd1c691ccc93161d59124d2877bd3afe9172d8 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Mon, 22 Jul 2024 13:31:29 -0400 Subject: [PATCH 7/9] ENH : comment + reg files --- MRI2CBCT/MRI2CBCT.py | 426 ++++++++++++++---- MRI2CBCT/Resources/UI/MRI2CBCT.ui | 51 ++- MRI2CBCT/utils/Method.py | 122 +---- MRI2CBCT/utils/Preprocess_CBCT.py | 201 +-------- MRI2CBCT/utils/Preprocess_CBCT_MRI.py | 96 ++-- MRI2CBCT/utils/Preprocess_MRI.py | 86 +--- MRI2CBCT/utils/Reg_MRI2CBCT.py | 117 +++++ MRI2CBCT/utils/utils_CBCT.py | 9 - .../normalize_percentile.py | 2 - MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py | 10 - .../MRI2CBCT_ORIENT_CENTER_MRI.py | 10 +- MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py | 26 +- MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml | 7 + .../MRI2CBCT_RESAMPLE_CBCT_MRI.py | 13 +- 14 files changed, 583 insertions(+), 593 deletions(-) create mode 100644 MRI2CBCT/utils/Reg_MRI2CBCT.py diff --git a/MRI2CBCT/MRI2CBCT.py b/MRI2CBCT/MRI2CBCT.py index 3890478..2c23d99 100644 --- a/MRI2CBCT/MRI2CBCT.py +++ b/MRI2CBCT/MRI2CBCT.py @@ -6,6 +6,7 @@ from utils.Preprocess_CBCT import Process_CBCT from utils.Preprocess_MRI import Process_MRI from utils.Preprocess_CBCT_MRI import Preprocess_CBCT_MRI +from utils.Reg_MRI2CBCT import Registration_MRI2CBCT import time import vtk @@ -41,7 +42,7 @@ def __init__(self, parent): ScriptedLoadableModule.__init__(self, parent) self.parent.title = _("MRI2CBCT") # TODO: make this more human readable by adding spaces # TODO: set categories (folders where the module shows up in the module selector) - self.parent.categories = [translate("qSlicerAbstractCoreModule", "Examples")] + self.parent.categories = ["Automated Dental Tools"] self.parent.dependencies = [] # TODO: add here list of module names that this module requires self.parent.contributors = ["John Doe (AnyWare Corp.)"] # TODO: replace with "Firstname Lastname (Organization)" # TODO: update with short description of the module and a link to online module documentation @@ -186,6 +187,7 @@ def setup(self) -> None: self.preprocess_cbct = Process_CBCT(self) self.preprocess_mri = Process_MRI(self) self.preprocess_mri_cbct = Preprocess_CBCT_MRI(self) + self.registration_mri2cbct = Registration_MRI2CBCT(self) # Connections # LineEditOutputReg @@ -194,7 +196,7 @@ def setup(self) -> None: self.addObserver(slicer.mrmlScene, slicer.mrmlScene.EndCloseEvent, self.onSceneEndClose) # Buttons - self.ui.applyButton.connect("clicked(bool)", self.onApplyButton) + self.ui.registrationButton.connect("clicked(bool)", self.registration_MR2CBCT) 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")) @@ -211,8 +213,22 @@ def setup(self) -> None: self.ui.pushButtonDownloadSegCBCT.connect("clicked(bool)",partial(self.downloadModel,self.ui.lineEditSegCBCT, "Segmentation", True)) - # Make sure parameter node is initialized (needed for module reload) + # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() + self.ui.ComboBoxCBCT.setCurrentIndex(1) + self.ui.ComboBoxCBCT.setEnabled(False) + self.ui.ComboBoxMRI.setCurrentIndex(1) + self.ui.ComboBoxMRI.setEnabled(False) + self.ui.comboBoxRegMRI.setCurrentIndex(1) + self.ui.comboBoxRegMRI.setEnabled(False) + self.ui.comboBoxRegCBCT.setCurrentIndex(1) + self.ui.comboBoxRegCBCT.setEnabled(False) + self.ui.comboBoxRegLabel.setCurrentIndex(1) + self.ui.comboBoxRegLabel.setEnabled(False) + + self.ui.label_time.setHidden(True) + self.ui.label_info.setHidden(True) + self.ui.progressBar.setHidden(True) self.ui.outputCollapsibleButton.setText("Registration") self.ui.inputsCollapsibleButton.setText("Preprocess") @@ -250,6 +266,7 @@ def setup(self) -> None: self.tableWidgetOrient.setCellWidget(row, col, checkBox) self.ui.ButtonDefaultOrientMRI.connect("clicked(bool)",self.defaultOrientMRI) + self.defaultOrientMRI() ################################################################################################## ### Normalization Table @@ -274,13 +291,19 @@ def setup(self) -> None: for row in range(2): for col in range(4): spinBox = QSpinBox() - spinBox.setMaximum(10000) + if col in [2, 3]: # Columns for Percentile Min and Percentile Max + spinBox.setMaximum(100) + else: + 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")) + + self.DefaultNorm("1",_) ################################################################################################## + # RESAMPLE TABLE self.tableWidgetResample = self.ui.tableWidgetResample # Increase the row and column count @@ -298,7 +321,7 @@ def setup(self) -> None: ) # Set the headers - self.tableWidgetResample.setHorizontalHeaderLabels(["X", "Y", "Z", "Keep File"]) + self.tableWidgetResample.setHorizontalHeaderLabels(["X", "Y", "Z", "Keep File "]) self.tableWidgetResample.setVerticalHeaderLabels(["Number of slices", "Spacing"]) # Add QSpinBoxes for the first row @@ -321,27 +344,45 @@ def setup(self) -> None: spinBox4 = QDoubleSpinBox() spinBox4.setMaximum(10000) spinBox4.setSingleStep(0.1) + spinBox4.setValue(0.3) self.tableWidgetResample.setCellWidget(1, 0, spinBox4) spinBox5 = QDoubleSpinBox() spinBox5.setMaximum(10000) spinBox5.setSingleStep(0.1) + spinBox5.setValue(0.3) self.tableWidgetResample.setCellWidget(1, 1, spinBox5) spinBox6 = QDoubleSpinBox() spinBox6.setMaximum(10000) spinBox6.setSingleStep(0.1) + spinBox6.setValue(0.3) self.tableWidgetResample.setCellWidget(1, 2, spinBox6) # Add QCheckBox for the "Keep File" column - checkBox1 = QCheckBox() + checkBox1 = QCheckBox("Keep the same size as the input scan") checkBox1.stateChanged.connect(lambda state: self.toggleSpinBoxes(state, [spinBox1, spinBox2, spinBox3])) self.tableWidgetResample.setCellWidget(0, 3, checkBox1) - checkBox2 = QCheckBox() + checkBox2 = QCheckBox("Keep the same spacing as the input scan") checkBox2.stateChanged.connect(lambda state: self.toggleSpinBoxes(state, [spinBox4, spinBox5, spinBox6])) self.tableWidgetResample.setCellWidget(1, 3, checkBox2) def toggleSpinBoxes(self, state, spinBoxes): + """ + Enable or disable a list of QSpinBox widgets based on the provided state. + + Parameters: + - state: An integer representing the state (2 for disabled, any other value for enabled). + - spinBoxes: A list of QSpinBox widgets to be toggled. + + The function iterates through each QSpinBox in the provided list. If the state is 2, + the QSpinBox is disabled and its text color is set to gray. Otherwise, the QSpinBox + is enabled and its default stylesheet is restored. + + This function is connected to the "keep file" checkbox. When the checkbox is checked + (state == 2), the spin boxes are disabled and shown in gray. If the checkbox is unchecked, + the spin boxes are enabled and restored to their default style. + """ for spinBox in spinBoxes: if state == 2: spinBox.setEnabled(False) @@ -357,6 +398,8 @@ def get_resample_values(self): :return: A tuple of two lists representing the resample values for the two rows. Each list contains three values (X, Y, Z) or None if the "Keep File" checkbox is checked. + First output : number of slices. + Second output : spacing """ resample_values_row1 = [] resample_values_row2 = [] @@ -386,6 +429,23 @@ def get_resample_values(self): def onCheckboxOrientClicked(self, row, col, state): + """ + Handle the click event of the orientation checkboxes in the table. + + Parameters: + - row: The row index of the clicked checkbox. + - col: The column index of the clicked checkbox. + - state: The state of the clicked checkbox (2 for checked, 0 for unchecked). + + This function updates the orientation checkboxes in the table based on the user's selection. + It ensures that only one checkbox per row can be set to '1' (or '-1' if the "Minus" column is checked) + and that the rest are set to '0'. Additionally, if the "Minus" column checkbox is checked, it sets + the text to 'Yes' and updates related checkboxes in the same row accordingly. The function also handles + unchecking a checkbox and updating the styles and texts of other checkboxes in the same row and column. + + This function is connected to the checkboxes for the orientation of the MRI. When a checkbox is clicked, + it ensures the correct orientation is set, following the specified rules. + """ if col == 3: # If the "Minus" column checkbox is clicked if state == 2: # Checkbox is checked self.minus_checked_rows.add(row) @@ -475,6 +535,16 @@ def onCheckboxOrientClicked(self, row, col, state): checkBox.setStyleSheet("font-weight: normal;") def getCheckboxValuesOrient(self): + """ + Retrieve the values of the orientation checkboxes in the table. + + This function iterates through each checkbox in a 3x3 grid within the tableWidgetOrient. + It collects the integer value (text) of each checkbox and stores them in a list, which is + then converted to a tuple and returned. + + Returns: + - A tuple containing the integer values of the checkboxes, representing the orientation of the MRI. + """ values = [] for row in range(3): for col in range(3): @@ -484,6 +554,21 @@ def getCheckboxValuesOrient(self): return tuple(values) def defaultOrientMRI(self): + """ + Set the default orientation values for the MRI checkboxes in the table. + + This function initializes the orientation of the MRI by setting specific checkboxes + to predefined values. It iterates through a list of initial states, where each state + is a tuple containing the row, column, and value to set. The value can be 1, -1, or 0. + The corresponding checkbox is checked and its text is set accordingly. Additionally, + the checkbox style is updated to make the checked state bold, and the respective sets + (checked_cells and minus_checked_rows) are updated. + + The initial states are: + - Row 0, Column 2: Set to -1 + - Row 1, Column 0: Set to 1 + - Row 2, Column 1: Set to -1 + """ initial_states = [ (0, 2, -1), (1, 0, 1), @@ -526,27 +611,15 @@ 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 pass def onSceneEndClose(self, caller, event) -> None: """Called just after the scene is closed.""" - # If this module is shown while the scene is closed then recreate a new parameter node immediately if self.parent.isEntered: self.initializeParameterNode() def initializeParameterNode(self) -> None: """Ensure parameter node exists and observed.""" - # 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()) - - # 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 pass @@ -554,6 +627,16 @@ def _checkCanApply(self, caller=None, event=None) -> None: pass def getNormalization(self): + """ + Retrieve the normalization values from the table. + + This function iterates through each cell in the tableWidgetNorm, collecting the values + of QSpinBox widgets. It stores these values in a nested list, where each sublist represents + a row of values. The collected values are then returned as a list of lists. + + Returns: + - A list of lists containing the values of the QSpinBox widgets in the tableWidgetNorm. + """ values = [] for row in range(self.tableWidgetNorm.rowCount): rowData = [] @@ -565,6 +648,15 @@ def getNormalization(self): return(values) def DefaultNorm(self,num : str,_)->None: + """ + Set default normalization values in the tableWidgetNorm based on the identifier 'num'. + + If 'num' is "1", set specific default values; otherwise, use another set of values. + + Parameters: + - num: Identifier to select the set of default values. + - _: Unused parameter. + """ # Define the default values for each cell if num=="1": default_values = [ @@ -589,9 +681,6 @@ def openFinder(self,nom : str,_) -> None : 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") @@ -645,52 +734,23 @@ def openFinder(self,nom : str,_) -> None : surface_folder = QFileDialog.getExistingDirectory(self.parent, "Select a scan folder") self.ui.LineEditOutput.setText(surface_folder) - - def onApplyButton(self) -> None: - self.list_Processes_Parameters=[] - # MRI2CBCT_ORIENT_CENTER_MRI - MRI2CBCT_RESAMPLE_REG = slicer.modules.mri2cbct_reg - parameter_mri2cbct_reg = { - "folder_general": self.ui.LineEditOutput.text, - "mri_folder": self.ui.lineEditRegMRI.text, - "cbct_folder": self.ui.lineEditRegCBCT.text, - "cbct_label2": self.ui.lineEditRegLabel.text, - "normalization" : [self.getNormalization()] - } - - self.list_Processes_Parameters.append( - { - "Process": MRI2CBCT_RESAMPLE_REG, - "Parameter": parameter_mri2cbct_reg, - "Module": "Resample files", - } - ) - """Run processing when user clicks "Apply" button.""" - print("get_normalization : ",self.getNormalization()) - - self.onProcessStarted() - - # /!\ Launch of the first process /!\ - print("module name : ",self.list_Processes_Parameters[0]["Module"]) - print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) - - self.process = slicer.cli.run( - self.list_Processes_Parameters[0]["Process"], - None, - self.list_Processes_Parameters[0]["Parameter"], - ) - - self.module_name = self.list_Processes_Parameters[0]["Module"] - self.processObserver = self.process.AddObserver( - "ModifiedEvent", self.onProcessUpdate - ) - - del self.list_Processes_Parameters[0] - # print("getCheckboxValuesOrient : ",self.getCheckboxValuesOrient()) def downloadModel(self, lineEdit, name, test,_): - """Function to download the model files from the link in the getModelUrl function""" + """ + Download model files from the URL(s) provided by the getModelUrl function. + + Parameters: + - lineEdit: The QLineEdit widget to update with the model folder path. + - name: The name of the model to download. + - test: A flag for testing purposes (unused in this function). + - _: Unused parameter for compatibility. + + This function fetches the model URL(s) using getModelUrl, downloads the files, + unzips them to the appropriate directory, and updates the lineEdit with the model + folder path. It also runs a test on the downloaded model and shows a warning message + if any errors occur. + """ # To select the reference files (CBCT Orientation and Registration mode only) listmodel = self.preprocess_cbct.getModelUrl() @@ -731,6 +791,20 @@ def downloadModel(self, lineEdit, name, test,_): def DownloadUnzip( self, url, directory, folder_name=None, num_downl=1, total_downloads=1 ): + """ + Download and unzip a file from a given URL to a specified directory. + + Parameters: + - url: The URL of the zip file to download. + - directory: The directory where the file should be downloaded and unzipped. + - folder_name: The name of the folder to create and unzip the contents into. + - num_downl: The current download number (for progress display). + - total_downloads: The total number of downloads (for progress display). + + Returns: + - out_path: The path to the unzipped folder. + """ + out_path = os.path.join(directory, folder_name) if not os.path.exists(out_path): @@ -785,15 +859,32 @@ def DownloadUnzip( return out_path def orientCBCT(self)->None: - self.list_Processes_Parameters = self.preprocess_cbct.Process( - input_t1_folder=self.ui.LineEditCBCT.text, - folder_output=self.ui.lineEditOutputOrientCBCT.text, - model_folder_1=self.ui.lineEditSegCBCT.text, - add_in_namefile="oui", - merge_seg=False, - isDCMInput=False, - slicerDownload=self.SlicerDownloadPath, - ) + """ + This function is called when the button "pushButtonOrientCBCT" is click. + Orient CBCT images using specified parameters and initiate the processing pipeline. + + This function sets up the parameters for CBCT image orientation, tests the process and scan, + and starts the processing pipeline if all checks pass. It handles the initial setup, + parameter passing, and process initiation, including setting up observers for process updates. + """ + + param = {"input_t1_folder":self.ui.LineEditCBCT.text, + "folder_output":self.ui.lineEditOutputOrientCBCT.text, + "model_folder_1":self.ui.lineEditSegCBCT.text, + "merge_seg":False, + "isDCMInput":False, + "slicerDownload":self.SlicerDownloadPath} + + ok,mess = self.preprocess_cbct.TestProcess(**param) + if not ok : + self.showMessage(mess) + return + ok,mess = self.preprocess_cbct.TestScan(param["input_t1_folder"]) + if not ok : + self.showMessage(mess) + return + + self.list_Processes_Parameters = self.preprocess_cbct.Process(**param) self.onProcessStarted() @@ -815,11 +906,29 @@ def orientCBCT(self)->None: del self.list_Processes_Parameters[0] def orientCenterMRI(self): - self.list_Processes_Parameters = self.preprocess_mri.Process( - input_folder=self.ui.LineEditMRI.text, - direction=self.getCheckboxValuesOrient(), - output_folder=self.ui.lineEditOutputOrientMRI.text, - ) + """ + This function is called when the button "pushButtonOrientMRI" is click. + Orient and center MRI images using specified parameters and initiate the processing pipeline. + + This function sets up the parameters for MRI image orientation and centering, tests the process and scan, + and starts the processing pipeline if all checks pass. It handles the initial setup, parameter passing, + and process initiation, including setting up observers for process updates. + """ + + param = {"input_folder":self.ui.LineEditMRI.text, + "direction":self.getCheckboxValuesOrient(), + "output_folder":self.ui.lineEditOutputOrientMRI.text} + + ok,mess = self.preprocess_mri.TestProcess(**param) + if not ok : + self.showMessage(mess) + return + ok,mess = self.preprocess_mri.TestScan(param["input_folder"]) + if not ok : + self.showMessage(mess) + return + + self.list_Processes_Parameters = self.preprocess_mri.Process(**param) self.onProcessStarted() @@ -841,7 +950,15 @@ def orientCenterMRI(self): del self.list_Processes_Parameters[0] def resampleMRICBCT(self): - print("self.ui.lineEditOutputOrientMRI.text : ",self.ui.lineEditOuputResample.text) + """ + Resample MRI and/or CBCT images based on the selected options and initiate the processing pipeline. + + This function determines which input folders (MRI, CBCT, or both) to use based on the user's selection + in the comboBoxResample widget. It sets up the resampling parameters, tests the process and scans, + and starts the processing pipeline if all checks pass. The function handles the initial setup, parameter + passing, and process initiation, including setting up observers for process updates. + """ + if self.ui.comboBoxResample.currentText=="CBCT": LineEditMRI = "None" LineEditCBCT = self.ui.LineEditCBCT.text @@ -852,16 +969,101 @@ def resampleMRICBCT(self): LineEditMRI = self.ui.LineEditMRI.text LineEditCBCT = self.ui.LineEditCBCT.text - print("self.get_resample_values() : ",self.get_resample_values()) + param = {"input_folder_MRI": LineEditMRI, + "input_folder_CBCT": LineEditCBCT, + "output_folder": self.ui.lineEditOuputResample.text, + "resample_size": self.get_resample_values()[0], + "spacing" : self.get_resample_values()[1] + } + + ok,mess = self.preprocess_mri_cbct.TestProcess(**param) + if not ok : + self.showMessage(mess) + return + + ok,mess = self.preprocess_mri_cbct.TestScan(param["input_folder_MRI"]) + + if not ok : + mess = mess + "MRI folder" + self.showMessage(mess) + return + + ok,mess = self.preprocess_mri_cbct.TestScan(param["input_folder_CBCT"]) + if not ok : + mess = mess + "CBCT folder" + self.showMessage(mess) + return + - self.list_Processes_Parameters = self.preprocess_mri_cbct.Process( - input_folder_MRI=LineEditMRI, - input_folder_CBCT=LineEditCBCT, - output_folder=self.ui.lineEditOuputResample.text, - resample_size=self.get_resample_values()[0], - spacing=self.get_resample_values()[1] + self.list_Processes_Parameters = self.preprocess_mri_cbct.Process(**param) + + self.onProcessStarted() + + # /!\ Launch of the first process /!\ + print("module name : ",self.list_Processes_Parameters[0]["Module"]) + print("Parameters : ",self.list_Processes_Parameters[0]["Parameter"]) + + self.process = slicer.cli.run( + self.list_Processes_Parameters[0]["Process"], + None, + self.list_Processes_Parameters[0]["Parameter"], ) + self.module_name = self.list_Processes_Parameters[0]["Module"] + self.processObserver = self.process.AddObserver( + "ModifiedEvent", self.onProcessUpdate + ) + + del self.list_Processes_Parameters[0] + + + def registration_MR2CBCT(self) -> None: + """ + Register MRI images to CBCT images using specified parameters and initiate the processing pipeline. + + This function sets up the parameters for MRI to CBCT registration, tests the process and scans, + and starts the processing pipeline if all checks pass. It handles the initial setup, parameter passing, + and process initiation, including setting up observers for process updates. The function also checks + for normalization parameters and validates input folders for the presence of necessary files. + """ + + param = {"folder_general": self.ui.LineEditOutput.text, + "mri_folder": self.ui.lineEditRegMRI.text, + "cbct_folder": self.ui.lineEditRegCBCT.text, + "cbct_label2": self.ui.lineEditRegLabel.text, + "normalization" : [self.getNormalization()], + "tempo_fold" : self.ui.checkBoxTompraryFold.isChecked()} + + ok,mess = self.registration_mri2cbct.TestProcess(**param) + if not ok : + self.showMessage(mess) + return + + ok1,mess = self.registration_mri2cbct.TestScan(param["mri_folder"]) + ok2,mess2 = self.registration_mri2cbct.TestScan(param["cbct_folder"]) + ok3,mess3 = self.registration_mri2cbct.TestScan(param["cbct_label2"]) + + error_messages = [] + + if not ok1: + error_messages.append("MRI folder") + if not ok2: + error_messages.append("CBCT folder") + if not ok3: + error_messages.append("CBCT label2 folder") + + if error_messages: + error_message = "No files to run has been found in the following folders: " + ", ".join(error_messages) + self.showMessage(error_message) + return + + ok,mess = self.registration_mri2cbct.CheckNormalization(param["normalization"]) + if not ok : + self.showMessage(mess) + return + + self.list_Processes_Parameters = self.registration_mri2cbct.Process(**param) + self.onProcessStarted() # /!\ Launch of the first process /!\ @@ -882,7 +1084,15 @@ def resampleMRICBCT(self): del self.list_Processes_Parameters[0] + def onProcessStarted(self): + """ + Initialize and update the UI components when a process starts. + + This function sets the start time, initializes the progress bar and related UI elements, + and updates the process-related attributes such as the number of extensions and modules. + It also enables the running state UI to reflect that a process is in progress. + """ self.startTime = time.time() # self.ui.progressBar.setMaximum(self.nb_patient) @@ -901,6 +1111,17 @@ def onProcessStarted(self): self.RunningUI(True) def onProcessUpdate(self, caller, event): + """ + Update the UI components during the process execution and handle process completion. + + This function updates the progress bar, time label, and information label during the process execution. + It handles the completion of each process step, manages errors, and initiates the next process if available. + + Parameters: + - caller: The process that triggered the update. + - event: The event that triggered the update. + """ + self.ui.progressBar.setVisible(False) # timer = f"Time : {time.time()-self.startTime:.2f}s" currentTime = time.time() - self.startTime @@ -913,7 +1134,7 @@ def onProcessUpdate(self, caller, event): self.ui.label_time.setText(timer) # self.module_name = caller.GetModuleTitle() if self.module_name_bis is None else self.module_name_bis - self.ui.label_info.setText(f"Extension {self.module_name} is running. \n Number of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}") + self.ui.label_info.setText(f"Extension {self.module_name} is running. \nNumber of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}") # self.displayModule = self.displayModule_bis if self.displayModule_bis is not None else self.display[self.module_name.split(' ')[0]] if self.module_name_before != self.module_name: @@ -922,7 +1143,7 @@ def onProcessUpdate(self, caller, event): self.ui.progressBar.setFormat(f"{100*self.nb_extnesion_did/self.nb_extension_launch}%") self.nb_extnesion_did += 1 self.ui.label_info.setText( - f"Extension {self.module_name} is running. \n Number of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}" + f"Extension {self.module_name} is running. \nNumber of extension runned : {self.nb_extnesion_did} / {self.nb_extension_launch}" ) @@ -967,15 +1188,20 @@ def onProcessUpdate(self, caller, event): self.OnEndProcess() def OnEndProcess(self): + """ + Finalize the process execution and update the UI components accordingly. + + This function increments the number of completed extensions, updates the information label, + resets the progress bar, calculates the total time taken, and displays a message box indicating + the completion of the process. It also disables the running state UI. + """ + self.nb_extnesion_did += 1 self.ui.label_info.setText( f"Process end" ) self.ui.progressBar.setValue(0) - # if self.nb_change_bystep == 0: - # print(f'Erreur this module didnt work {self.module_name_before}') - self.module_name_before = self.module_name self.nb_change_bystep = 0 total_time = time.time() - self.startTime @@ -996,7 +1222,7 @@ def OnEndProcess(self): msg.setIcon(QMessageBox.Information) # setting message for Message Box - msg.setText(f"Processing completed in {stopTime-self.startTime:.2f} seconds") + msg.setText(f"Processing completed in {int(total_time / 60)} min and {int(total_time % 60)} sec") # setting Message box window title msg.setWindowTitle("Information") @@ -1016,6 +1242,20 @@ def RunningUI(self, run=False): self.ui.progressBar.setVisible(run) self.ui.label_time.setVisible(run) self.ui.label_info.setVisible(run) + + def showMessage(self,mess): + msg = QMessageBox() + msg.setIcon(QMessageBox.Information) + + # setting message for Message Box + msg.setText(mess) + + # setting Message box window title + msg.setWindowTitle("Information") + + # declaring buttons on Message Box + msg.setStandardButtons(QMessageBox.Ok) + msg.exec_() diff --git a/MRI2CBCT/Resources/UI/MRI2CBCT.ui b/MRI2CBCT/Resources/UI/MRI2CBCT.ui index 2de50c5..b7d7e11 100644 --- a/MRI2CBCT/Resources/UI/MRI2CBCT.ui +++ b/MRI2CBCT/Resources/UI/MRI2CBCT.ui @@ -12,8 +12,8 @@ - - + + Inputs @@ -90,7 +90,7 @@ - _________________________________________________________ + __________________________________________________________________________________________________________________________________ @@ -172,7 +172,7 @@ - ______________________________________________________________________________________________ + __________________________________________________________________________________________________________________________________ @@ -245,7 +245,7 @@ - ______________________________________________________________________________________________ + __________________________________________________________________________________________________________________________________ @@ -314,8 +314,8 @@ - - + + Output @@ -326,13 +326,6 @@ - - - - Search - - - @@ -343,6 +336,20 @@ + + + + Suffix : + + + + + + + Search + + + @@ -350,10 +357,10 @@ - - + + - Suffix : + Keep the temporary folder @@ -490,7 +497,7 @@ - + true @@ -557,15 +564,15 @@ - qMRMLWidget + ctkCollapsibleButton QWidget -
qMRMLWidget.h
+
ctkCollapsibleButton.h
1
- ctkCollapsibleButton + qMRMLWidget QWidget -
ctkCollapsibleButton.h
+
qMRMLWidget.h
1
diff --git a/MRI2CBCT/utils/Method.py b/MRI2CBCT/utils/Method.py index 4825b65..d81a426 100644 --- a/MRI2CBCT/utils/Method.py +++ b/MRI2CBCT/utils/Method.py @@ -24,7 +24,7 @@ def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): pass @abstractmethod - def TestScan(self, scan_folder_t1: str, scan_folder_t2) -> str: + def TestScan(self, scan_folder_t1: str, scan_folder_t2): """Verify if the input folder seems good (have everything required to run the mode selected), if something is wrong the function return string with error message This function is called when the user want to import scan @@ -33,39 +33,11 @@ def TestScan(self, scan_folder_t1: str, scan_folder_t2) -> str: scan_folder (str): path of folder with scan Returns: - str or None: Return str with error message if something is wrong, else return None + str and bool: Return str with error message if something is wrong and a boolean to indicate if there is a message pass """ - @abstractmethod - def TestReference(self, ref_folder: str) -> str: - """Verify if the reference folder contains reference gold files with landmarks and scans, if True return None and if False return str with error message to user - - Args: - ref_folder (str): folder path with gold landmark - - Return : - str or None : display str to user like warning - """ - - pass - - @abstractmethod - def TestModel(self, model_folder: str, lineEditName) -> str: - """Verify whether the model folder contains the right models used for ALI and other AI tool - - Args: - model_folder (str): folder path with different models - - Return : - str or None : display str to user like warning - """ - pass - - @abstractmethod - def TestCheckbox(self) -> str: - pass @abstractmethod def TestProcess(self, **kwargs) -> str: @@ -84,88 +56,6 @@ def Process(self, **kwargs): pass - @abstractmethod - def DicLandmark(self): - """ - return dic landmark like this: - dic = {'teeth':{ - 'Lower':['LR6','LR5',...], - 'Upper':['UR6',...] - }, - 'Landmark':{ - 'Occlusual':['O',...], - 'Cervical':['R',...] - } - } - """ - - pass - - @abstractmethod - def existsLandmark(self, pathfile: str, pathref: str, pathmodel: str): - """return dictionnary. when the value of the landmark in dictionnary is true, the landmark is in input folder and in gold folder - Args: - pathfile (str): path - - Return : - dict : exemple dic = {'O':True,'UL6':False,'UR1':False,...} - """ - pass - - @abstractmethod - def getTestFileList(self): - """Return a tuple with both the name and the Download link of the test files - - tuple = ('name','link') - """ - pass - - @abstractmethod - def getReferenceList(self): - """ - Return a dictionnary with both the name and the Download link of the references - - dict = {'name1':'link1','name2':'link2',...} - - """ - pass - - @abstractmethod - def getModelUrl(self): - """ - Return dictionnary contains the url for each model - - dict = {'name':{'type1':'url1','type2':'url2'},...} - or - dict = {'name':'url'} - - """ - pass - - @abstractmethod - def getALIModelList(self): - """ - Return a tuple with both the name and the Download link for ALI model - else: - name, url = self.ActualMeth.getTestFileList() - - tuple = ('name','link') - - """ - pass - - def getcheckbox(self): - return self.diccheckbox - - def setcheckbox(self, dicccheckbox): - self.diccheckbox = dicccheckbox - - def getcheckbox2(self): - return self.diccheckbox2 - - def setcheckbox2(self, dicccheckbox): - self.diccheckbox2 = dicccheckbox - def search(self, path, *args): """ Return a dictionary with args element as key and a list of file in path directory finishing by args extension for each key @@ -197,14 +87,6 @@ def search(self, path, *args): } - def ListLandmarksJson(self, json_file): - with open(json_file) as f: - data = json.load(f) - - return [ - data["markups"][0]["controlPoints"][i]["label"] - for i in range(len(data["markups"][0]["controlPoints"])) - ] def getTestFileListDCM(self): """Return a tuple with both the name and the Download link of the test files but only for DCM files (AREG CBCT) diff --git a/MRI2CBCT/utils/Preprocess_CBCT.py b/MRI2CBCT/utils/Preprocess_CBCT.py index a7294c7..a93b6f2 100644 --- a/MRI2CBCT/utils/Preprocess_CBCT.py +++ b/MRI2CBCT/utils/Preprocess_CBCT.py @@ -20,9 +20,6 @@ def __init__(self, widget): self.tempAMASSS_folder = os.path.join( documents, slicer.app.applicationName + "_temp_AMASSS" ) - self.tempALI_folder = os.path.join( - documents, slicer.app.applicationName + "_temp_ALI" - ) def getGPUUsage(self): if platform.system() == "Darwin": @@ -33,75 +30,45 @@ def getGPUUsage(self): def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) - def getReferenceList(self): - return { - "Occlusal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Occlusal_Midsagittal_Plane.zip", - "Frankfurt Horizontal and Midsagittal Plane": "https://github.com/lucanchling/ASO_CBCT/releases/download/v01_goldmodels/Frankfurt_Horizontal_Midsagittal_Plane.zip", - } - - def TestReference(self, ref_folder: str): - out = None - scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] - lm_extension = [".json"] - - if self.NumberScan(ref_folder) == 0: - out = "The selected folder must contain scans" - - if self.NumberScan(ref_folder) > 1: - out = "The selected folder must contain only 1 case" - return None - def TestCheckbox(self, dic_checkbox): - list_landmark = self.CheckboxisChecked(dic_checkbox)[ - "Regions of Reference for Registration" - ] - - out = None - if len(list_landmark) == 0: - out = "Please select a Registration Type\n" - return out - - def TestModel(self, model_folder: str, lineEditName) -> str: - - if lineEditName == "lineEditSegCBCT": + def TestModel(self, model_folder: str,lineEdit:str) -> str: + if lineEdit == "lineEditSegCBCT": if len(super().search(model_folder, "pth")["pth"]) == 0: return "Folder must have models for mask segmentation" else: return None - # if lineEditName == 'lineEditModelAli': - # if len(super().search(model_folder,'pth')['pth']) == 0: - # return 'Folder must have ALI models files' - # else: - # return None + def TestScan(self, scan_folder: str): + extensions = ['.nii', '.nii.gz', '.nrrd'] + found_files = self.search(scan_folder, extensions) + if any(found_files[ext] for ext in extensions): + return True, "" + else: + return False, "No files to run has been found in the input folder" + + def TestProcess(self, **kwargs) -> str: out = "" - - testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) - if testcheckbox is not None: - out += testcheckbox + ok = True if kwargs["input_t1_folder"] == "": - out += "Please select an input folder for T1 scans\n" - - if kwargs["input_t2_folder"] == "": - out += "Please select an input folder for T2 scans\n" + out += "Please select an input folder for CBCT scans\n" + ok = False if kwargs["folder_output"] == "": out += "Please select an output folder\n" - - if kwargs["add_in_namefile"] == "": - out += "Please select an extension for output files\n" + ok = False if kwargs["model_folder_1"] == "": out += "Please select a folder for segmentation models\n" - + ok = False + if out == "": out = None - return out + return ok,out def getModelUrl(self): return { @@ -116,136 +83,10 @@ def getModelUrl(self): }, } - def getALIModelList(self): - return ( - "ALIModels", - "https://github.com/lucanchling/ALI_CBCT/releases/download/models_v01/", - ) - - def ProperPrint(self, notfound_list): - dic = { - "scanT1": "T1 scan", - "scanT2": "T2 scan", - "segT1": "T1 segmentation", - "segT2": "T2 segmentation", - } - out = "" - if "scanT1" in notfound_list and "scanT2" in notfound_list: - out += "T1 and T2 scans\n" - elif "segT1" in notfound_list and "segT2" in notfound_list: - out += "T1 and T2 segmentations\n" - else: - for notfound in notfound_list: - out += dic[notfound] + " " - return out - - def TestScan( - self, - scan_folder_t1: str, - scan_folder_t2: str, - liste_keys=["scanT1", "scanT2", "segT1"], - ): - out = "" - scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] - if self.NumberScan(scan_folder_t1, scan_folder_t2) == 0: - return "Please Select folder with scans" - - patients = GetDictPatients(scan_folder_t1, scan_folder_t2) - for patient, data in patients.items(): - not_found = [key for key in liste_keys if key not in data.keys()] - if len(not_found) != 0: - out += ( - f"Patient {patient} does not have {self.ProperPrint(not_found)}\n" - ) - - if out == "": # If no errors - out = None - - return out - - def GetSegmentationLabel(self, seg_folder): - seg_label = [] - patients = GetPatients(seg_folder) - seg_path = patients[list(patients.keys())[0]]["segT1"] - seg = sitk.ReadImage(seg_path) - seg_array = sitk.GetArrayFromImage(seg) - labels = np.unique(seg_array) - for label in labels: - if label != 0 and label not in seg_label: - seg_label.append(label) - return seg_label - - def CheckboxisChecked(self, diccheckbox: dict, in_str=False): - listchecked = {key: [] for key in diccheckbox.keys()} - for key, checkboxs in diccheckbox.items(): - for checkbox in checkboxs: - if checkbox.isChecked(): - listchecked[key] += [checkbox.text] - - return listchecked - - def DicLandmark(self): - return { - "Regions of Reference for Registration": [ - "Cranial Base", - "Mandible", - "Maxilla", - ], - "AMASSS Segmentation": [ - "Cranial Base", - "Cervical Vertebra", - "Mandible", - "Maxilla", - "Skin", - "Upper Airway", - ], - } - - def TranslateModels(self, listeModels, mask=False): - dicTranslate = { - "Models": { - "Mandible": "MAND", - "Maxilla": "MAX", - "Cranial Base": "CB", - "Cervical Vertebra": "CV", - "Root Canal": "RC", - "Mandibular Canal": "MCAN", - "Upper Airway": "UAW", - "Skin": "SKIN", - }, - "Masks": { - "Cranial Base": "CBMASK", - "Mandible": "MANDMASK", - "Maxilla": "MAXMASK", - }, - } - - translate = "" - for i, model in enumerate(listeModels): - if i < len(listeModels) - 1: - if mask: - translate += dicTranslate["Masks"][model] + " " - else: - translate += dicTranslate["Models"][model] + " " - else: - if mask: - translate += dicTranslate["Masks"][model] - else: - translate += dicTranslate["Models"][model] - - return translate - - def existsLandmark(self, input_dir, reference_dir, model_dir): - return None - - def getTestFileList(self): - return ( - "Semi-Automated", - "https://github.com/lucanchling/Areg_CBCT/releases/download/TestFiles/SemiAuto.zip", - ) def Process(self, **kwargs): - centered_T1 = kwargs["input_t1_folder"] + "_Center" + centered_T1 = kwargs["folder_output"] + "CBCT_Center" + centered_T1 = os.path.join(kwargs["folder_output"], "_CBCT_Center") parameter_pre_aso = { "input": kwargs["input_t1_folder"], "output_folder": centered_T1, @@ -279,7 +120,7 @@ def Process(self, **kwargs): "merge": "MERGE" if kwargs["merge_seg"] else "SEPARATE", "genVtk": True, "save_in_folder": True, - "output_folder": kwargs["folder_output"], + "output_folder": os.path.join(kwargs["folder_output"],'CBCT_Segmentation'), "precision": 50, "vtk_smooth": 5, "prediction_ID": "Pred", diff --git a/MRI2CBCT/utils/Preprocess_CBCT_MRI.py b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py index 3794808..6285c35 100644 --- a/MRI2CBCT/utils/Preprocess_CBCT_MRI.py +++ b/MRI2CBCT/utils/Preprocess_CBCT_MRI.py @@ -17,7 +17,6 @@ def __init__(self, widget): super().__init__(widget) documentsLocation = qt.QStandardPaths.DocumentsLocation documents = qt.QStandardPaths.writableLocation(documentsLocation) - def getGPUUsage(self): if platform.system() == "Darwin": @@ -26,86 +25,49 @@ def getGPUUsage(self): return 5 def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): - # return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) - return 0 - - def getReferenceList(self): - pass - - def TestReference(self, ref_folder: str): - out = None - scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] - lm_extension = [".json"] - - if self.NumberScan(ref_folder) == 0: - out = "The selected folder must contain scans" - - if self.NumberScan(ref_folder) > 1: - out = "The selected folder must contain only 1 case" + return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) - return None - def TestCheckbox(self, dic_checkbox): - pass + def TestScan(self, scan_folder: str): + extensions = ['.nii', '.nii.gz', '.nrrd'] + if scan_folder!="None" : + found_files = self.search(scan_folder, extensions) + if any(found_files[ext] for ext in extensions): + return True, "" + else: + return False, "No files to run has been found in the " + return True,"" + + def TestProcess(self, **kwargs) -> str: out = "" + ok = True - testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) - if testcheckbox is not None: - out += testcheckbox - - if kwargs["input_folder"] == "": + if kwargs["input_folder_MRI"] == "": out += "Please select an input folder for MRI scans\n" + ok = False - if kwargs["direction"] == "": - out += "Please select a direction for every axe\n" + if kwargs["input_folder_CBCT"] == "": + out += "Please select an input folder for CBCT scans\n" + ok = False if kwargs["output_folder"] == "": out += "Please select an output folder\n" - + ok = False + + if kwargs["resample_size"] == "": + out += "Please select a new resample size\n" + ok = False + + if kwargs["spacing"] == "": + out += "Please select a new spacing\n" + ok = False + if out == "": out = None - return out - - def getModelUrl(self): - pass - - def getALIModelList(self): - pass - - def TestModel(self, model_folder: str, lineEditName): - pass - - def ProperPrint(self, notfound_list): - pass - - def TestScan( - self, - scan_folder_t1: str, - scan_folder_t2: str, - liste_keys=["scanT1", "scanT2", "segT1"], - ): - pass - - def GetSegmentationLabel(self, seg_folder): - pass - - def CheckboxisChecked(self, diccheckbox: dict, in_str=False): - pass - - def DicLandmark(self): - pass - - def TranslateModels(self, listeModels, mask=False): - pass - - def existsLandmark(self, input_dir, reference_dir, model_dir): - return None - - def getTestFileList(self): - pass + return ok,out def Process(self, **kwargs): list_process=[] diff --git a/MRI2CBCT/utils/Preprocess_MRI.py b/MRI2CBCT/utils/Preprocess_MRI.py index d8dc9a4..372a47b 100644 --- a/MRI2CBCT/utils/Preprocess_MRI.py +++ b/MRI2CBCT/utils/Preprocess_MRI.py @@ -17,7 +17,6 @@ def __init__(self, widget): super().__init__(widget) documentsLocation = qt.QStandardPaths.DocumentsLocation documents = qt.QStandardPaths.writableLocation(documentsLocation) - def getGPUUsage(self): if platform.system() == "Darwin": @@ -26,87 +25,40 @@ def getGPUUsage(self): return 5 def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): - # return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) - return 0 - - def getReferenceList(self): - pass - - def TestReference(self, ref_folder: str): - out = None - scan_extension = [".nrrd", ".nrrd.gz", ".nii", ".nii.gz", ".gipl", ".gipl.gz"] - lm_extension = [".json"] + return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) - if self.NumberScan(ref_folder) == 0: - out = "The selected folder must contain scans" - if self.NumberScan(ref_folder) > 1: - out = "The selected folder must contain only 1 case" - - return None - - def TestCheckbox(self, dic_checkbox): - pass + def TestScan(self, scan_folder: str): + extensions = ['.nii', '.nii.gz', '.nrrd'] + found_files = self.search(scan_folder, extensions) + if any(found_files[ext] for ext in extensions): + return True, "" + else: + return False, "No files to run has been found in the input folder" + + def TestProcess(self, **kwargs) -> str: out = "" - - testcheckbox = self.TestCheckbox(kwargs["dic_checkbox"]) - if testcheckbox is not None: - out += testcheckbox + ok = True if kwargs["input_folder"] == "": - out += "Please select an input folder for MRI scans\n" - - if kwargs["direction"] == "": - out += "Please select a direction for every axe\n" + out += "Please select an input folder for CBCT scans\n" + ok = False if kwargs["output_folder"] == "": out += "Please select an output folder\n" + ok = False + if kwargs["direction"] == "": + out += "Please select a new direction for X,Y and Z\n" + ok = False + if out == "": out = None - return out - - def getModelUrl(self): - pass - - def getALIModelList(self): - pass - - def TestModel(self, model_folder: str, lineEditName): - pass + return ok,out - def ProperPrint(self, notfound_list): - pass - - def TestScan( - self, - scan_folder_t1: str, - scan_folder_t2: str, - liste_keys=["scanT1", "scanT2", "segT1"], - ): - pass - - def GetSegmentationLabel(self, seg_folder): - pass - - def CheckboxisChecked(self, diccheckbox: dict, in_str=False): - pass - - def DicLandmark(self): - pass - - def TranslateModels(self, listeModels, mask=False): - pass - - def existsLandmark(self, input_dir, reference_dir, model_dir): - return None - - def getTestFileList(self): - pass - def Process(self, **kwargs): list_process=[] # MRI2CBCT_ORIENT_CENTER_MRI diff --git a/MRI2CBCT/utils/Reg_MRI2CBCT.py b/MRI2CBCT/utils/Reg_MRI2CBCT.py new file mode 100644 index 0000000..5ca93c0 --- /dev/null +++ b/MRI2CBCT/utils/Reg_MRI2CBCT.py @@ -0,0 +1,117 @@ +from utils.Method import Method +from utils.utils_CBCT import GetDictPatients, GetPatients +import os, sys + +import SimpleITK as sitk +import numpy as np + +from glob import iglob +import slicer +import time +import qt +import platform +import re + + +class Registration_MRI2CBCT(Method): + def __init__(self, widget): + super().__init__(widget) + documentsLocation = qt.QStandardPaths.DocumentsLocation + documents = qt.QStandardPaths.writableLocation(documentsLocation) + + def getGPUUsage(self): + if platform.system() == "Darwin": + return 1 + else: + return 5 + + def NumberScan(self, scan_folder_t1: str, scan_folder_t2: str): + return len(GetDictPatients(scan_folder_t1, scan_folder_t2)) + + + def TestScan(self, scan_folder: str): + extensions = ['.nii', '.nii.gz', '.nrrd'] + found_files = self.search(scan_folder, extensions) + if any(found_files[ext] for ext in extensions): + return True, "" + else: + return False, "No files to run has been found in the input folder" + + def CheckNormalization(self, norm: str): + mri_min_norm, mri_max_norm, mri_lower_p, mri_upper_p = norm[0][0] + cbct_min_norm, cbct_max_norm, cbct_lower_p, cbct_upper_p = norm[0][1] + + ok = True + messages = [] + + if mri_max_norm <= mri_min_norm: + ok = False + messages.append("MRI normalization max must be greater than min") + if mri_upper_p <= mri_lower_p: + ok = False + messages.append("MRI percentile max must be greater than min") + + if cbct_max_norm <= cbct_min_norm: + ok = False + messages.append("CBCT normalization max must be greater than min") + if cbct_upper_p <= cbct_lower_p: + ok = False + messages.append("CBCT percentile max must be greater than min") + + message = "\n".join(messages) + + return ok, message + + def TestProcess(self, **kwargs) -> str: + out = "" + ok = True + + if kwargs["folder_general"] == "": + out += "Please select an input folder for CBCT scans\n" + ok = False + + if kwargs["mri_folder"] == "": + out += "Please select an input folder for MRI scans\n" + ok = False + + if kwargs["cbct_folder"] == "": + out += "Please select an input folder for CBCT scans\n" + ok = False + + if kwargs["cbct_label2"] == "": + out += "Please select an input folder for CBCT segmentation\n" + ok = False + + if kwargs["normalization"] == "": + out += "Please select some values for the normalization\n" + ok = False + + if out == "": + out = None + + return ok,out + + def Process(self, **kwargs): + list_process=[] + + MRI2CBCT_RESAMPLE_REG = slicer.modules.mri2cbct_reg + parameter_mri2cbct_reg = { + "folder_general": kwargs["folder_general"], + "mri_folder": kwargs["mri_folder"], + "cbct_folder": kwargs["cbct_folder"], + "cbct_label2": kwargs["cbct_label2"], + "normalization" : kwargs["normalization"], + "tempo_fold" : kwargs["tempo_fold"] + } + + list_process.append( + { + "Process": MRI2CBCT_RESAMPLE_REG, + "Parameter": parameter_mri2cbct_reg, + "Module": "Resample files", + } + ) + + return list_process + + diff --git a/MRI2CBCT/utils/utils_CBCT.py b/MRI2CBCT/utils/utils_CBCT.py index a4c247a..84c7291 100644 --- a/MRI2CBCT/utils/utils_CBCT.py +++ b/MRI2CBCT/utils/utils_CBCT.py @@ -1,15 +1,6 @@ import os from glob import iglob -def GetListNamesSegType(segmentationType): - dic = { - "CB": ["cb"], - "MAND": ["mand", "md"], - "MAX": ["max", "mx"], - } - return dic[segmentationType] - - def GetListFiles(folder_path, file_extension): """Return a list of files in folder_path finishing by file_extension""" file_list = [] diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py index bc01d46..2248c1c 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/normalize_percentile.py @@ -30,7 +30,6 @@ def enhance_contrast(image,upper_percentile,lower_percentile, min_norm, max_norm """ # Compute thresholds lower_threshold, upper_threshold = compute_thresholds(image,lower_percentile,upper_percentile) - # print(f"Computed thresholds - Lower: {lower_threshold}, Upper: {upper_threshold}") # Normalize the image using the computed thresholds @@ -58,7 +57,6 @@ def normalize(input_folder, output_folder,upper_percentile,lower_percentile,min_ for filename in os.listdir(input_folder): if filename.endswith('.nii.gz'): input_path = os.path.join(input_folder, filename) - print("filename : ",filename) img = sitk.ReadImage(input_path) # Enhance the contrast of the image diff --git a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py index cef2d38..1a5c6ac 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py +++ b/MRI2CBCT_CLI/MRI2CBCT_CLI_utils/resample.py @@ -25,7 +25,6 @@ def resample_fn(img, args): output_size = args['size'] fit_spacing = args['fit_spacing'] iso_spacing = args['iso_spacing'] - print("iso_spacing : ",iso_spacing) pixel_dimension = args['pixel_dimension'] center = args['center'] @@ -50,10 +49,8 @@ def resample_fn(img, args): if(iso_spacing=="True"): output_spacing_filtered = [sp for si, sp in zip(args['size'], output_spacing) if si != -1] - # print(output_spacing_filtered) max_spacing = np.max(output_spacing_filtered) output_spacing = [sp if si == -1 else max_spacing for si, sp in zip(args['size'], output_spacing)] - # print(output_spacing) if(args['spacing'] is not None): @@ -67,11 +64,6 @@ def resample_fn(img, args): input_physical_size = np.array(size)*np.array(spacing) output_origin = np.array(output_origin) - (output_physical_size - input_physical_size)/2.0 - print("Input size:", size) - print("Input spacing:", spacing) - print("Output size:", output_size) - print("Output spacing:", output_spacing) - print("Output origin:", output_origin) resampleImageFilter = sitk.ResampleImageFilter() resampleImageFilter.SetInterpolator(InterpolatorType) @@ -106,7 +98,6 @@ def Resample(img_filename, args): - Returns the resampled image. """ - print("Reading:", img_filename) img = sitk.ReadImage(img_filename) if(args['img_spacing']): @@ -175,7 +166,6 @@ def resample_images(args): print("WARNING: Pixel size not supported!") if args['ref'] is not None: - print(args['ref']) ref = sitk.ReadImage(args['ref']) args['size'] = ref.GetSize() args['spacing'] = ref.GetSpacing() diff --git a/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py index 59c826f..8d1020f 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py +++ b/MRI2CBCT_CLI/MRI2CBCT_ORIENT_CENTER_MRI/MRI2CBCT_ORIENT_CENTER_MRI.py @@ -4,8 +4,6 @@ import SimpleITK as sitk import argparse -# THIS FILE IS WORKING WELL - def extract_id(filename): """ Extracts and returns the ID from a filename, removing common NIfTI extensions. @@ -45,9 +43,6 @@ def modify_image_properties(nifti_file_path, new_direction, output_file_path=Non Read a NIfTI file, change its Direction and optionally center and save the modified image. """ image = sitk.ReadImage(nifti_file_path) - print("Original Direction:", image.GetDirection()) - print("Original Origin:", image.GetOrigin()) - # Set the new direction image.SetDirection(new_direction) @@ -55,9 +50,6 @@ def modify_image_properties(nifti_file_path, new_direction, output_file_path=Non new_origin = calculate_new_origin(image) image.SetOrigin(new_origin) - print("New Direction:", image.GetDirection()) - print("New Origin:", image.GetOrigin()) - if output_file_path: sitk.WriteImage(image, output_file_path) print(f"Modified image saved to {output_file_path}") @@ -87,7 +79,7 @@ def main(args): if type_file==0: output_file_path = os.path.join(output_folder, f"{file_id}_OR.nii") else : - output_file_path = os.path.join(output_folder, f"{file_id}_OR.nii") + output_file_path = os.path.join(output_folder, f"{file_id}_OR.nii.gz") modify_image_properties(file_path, new_direction, output_file_path) if __name__ == "__main__": diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py index 71c7dd9..7569335 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.py @@ -1,10 +1,9 @@ #!/usr/bin/env python-real -import subprocess import argparse import os import re - +import shutil import sys fpath = os.path.join(os.path.dirname(__file__), "..") @@ -102,7 +101,7 @@ def run_script_AREG_MRI_folder(cbct_folder, cbct_mask_folder,mri_folder,mri_orig cbct_max_norm (float): Maximum value for CBCT normalization. """ - output_folder = os.path.join(folder_general,"z01_output",f"a01_mri:inv+norm[{mri_min_norm},{mri_max_norm}]+p[{mri_lower_p},{mri_upper_p}]_cbct:norm[{cbct_min_norm},{cbct_max_norm}]+p[{cbct_lower_p},{cbct_upper_p}]+mask") + output_folder = os.path.join(folder_general,f"mri:inv+norm[{mri_min_norm},{mri_max_norm}]+p[{mri_lower_p},{mri_upper_p}]_cbct:norm[{cbct_min_norm},{cbct_max_norm}]+p[{cbct_lower_p},{cbct_upper_p}]+mask") create_folder(output_folder) registration(cbct_folder,mri_folder,cbct_mask_folder,output_folder,mri_original_folder) return cbct_mask_folder @@ -126,6 +125,13 @@ def extract_values(input_string): return a, b, c, d, e, f, g, h +def delete_folder(folder_path): + if os.path.exists(folder_path): + shutil.rmtree(folder_path) + print(f"The folder '{folder_path}' has been deleted successfully.") + else: + print(f"The folder '{folder_path}' does not exist.") + def main(): parser = argparse.ArgumentParser(description="Run multiple Python scripts with arguments") parser.add_argument('folder_general', type=str, help="Folder general where to make all the output") @@ -133,6 +139,7 @@ def main(): parser.add_argument('cbct_folder', type=str, help="Folder containing original CBCT images.") parser.add_argument('cbct_label2', type=str, help="Folder containing CBCT masks.") parser.add_argument('normalization', type=str, help="Folder containing CBCT masks.") + parser.add_argument('tempo_fold', type=str, help="Indicate to keep the temporary fold or not") args = parser.parse_args() mri_min_norm, mri_max_norm, mri_lower_p, mri_upper_p, cbct_min_norm, cbct_max_norm, cbct_lower_p, cbct_upper_p = extract_values(args.normalization) @@ -144,12 +151,19 @@ def main(): # CBCT output_path_norm_cbct = run_script_normalize_percentile("CBCT",args.cbct_folder, args.folder_general, upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) input_path_cbct_norm_mask = run_script_apply_mask(output_path_norm_cbct,args.cbct_label2,args.folder_general,"mask",upper_percentile=cbct_upper_p, lower_percentile=cbct_lower_p, max_norm=cbct_max_norm, min_norm=cbct_min_norm) - print('input_path_cbct_norm_mask : ',input_path_cbct_norm_mask) # REG - print("*"*100) run_script_AREG_MRI_folder(cbct_folder=args.cbct_folder,cbct_mask_folder=input_path_cbct_norm_mask,mri_folder=input_path_norm_mri,mri_original_folder=args.mri_folder,folder_general=args.folder_general,mri_lower_p=mri_lower_p,mri_upper_p=mri_upper_p,mri_min_norm=mri_min_norm,mri_max_norm=mri_max_norm,cbct_lower_p=cbct_lower_p,cbct_upper_p=cbct_upper_p,cbct_min_norm=cbct_min_norm,cbct_max_norm=cbct_max_norm) - print("EENNNDDD") + + + if args.tempo_fold=="false": + delete_folder(folder_mri_inverse) + delete_folder(input_path_norm_mri) + delete_folder(os.path.dirname(input_path_norm_mri)) + delete_folder(output_path_norm_cbct) + delete_folder(os.path.dirname(output_path_norm_cbct)) + delete_folder(input_path_cbct_norm_mask) + delete_folder(os.path.dirname(input_path_cbct_norm_mask)) diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml index 7d53ee0..964675e 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml @@ -50,5 +50,12 @@ Normalization to use for MRI and CBCT + + tempo_fold + + 5 + If keeping temporary fold or not + + diff --git a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py index bafc887..0d84e04 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py +++ b/MRI2CBCT_CLI/MRI2CBCT_RESAMPLE_CBCT_MRI/MRI2CBCT_RESAMPLE_CBCT_MRI.py @@ -43,7 +43,6 @@ def run_resample(img=None, dir=None, csv=None, csv_column='image', csv_root_path 'out': out, 'out_ext': out_ext, } - print("args : ",args) resample_images(args) def transform_size(size_str): @@ -68,19 +67,17 @@ def main(input_folder,output_folder,resample_size,spacing,iso_spacing): csv_reader = csv.DictReader(csv_file) for row in csv_reader: size_file = tuple(map(int, row["size"].strip("()").split(","))) + spacing_file = tuple(map(float, row["Spacing"].strip("()").split(","))) input_path = row["in"] out_path = row["out"] if resample_size != "None" and spacing=="None" : - print("1"*100) - run_resample(img=input_path,out=out_path,size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=iso_spacing,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + run_resample(img=input_path,out=out_path,size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=False,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) elif resample_size == "None" and spacing!="None" : - print("2"*100) - run_resample(img=input_path,out=out_path,img_spacing=list(map(float, spacing.split(','))),size=[size_file[0],size_file[1],size_file[2]],fit_spacing=True,center=0,iso_spacing=True,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + run_resample(img=input_path,out=out_path,spacing=list(map(float, spacing.split(','))),size=[size_file[0],size_file[1],size_file[2]],fit_spacing=False,center=0,iso_spacing=False,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) elif resample_size != "None" and spacing!="None" : - print("3"*100) - run_resample(img=input_path,out=out_path,img_spacing=list(map(float, spacing.split(','))),size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=True,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) + run_resample(img=input_path,out=out_path,spacing=list(map(float, spacing.split(','))),size=list(map(int, resample_size.split(','))),fit_spacing=True,center=0,iso_spacing=False,linear=False,image_dimension=3,pixel_dimension=1,rgb=False,ow=0) - # delete_csv(csv_path) + delete_csv(csv_path) def delete_csv(file_path): """Delete a CSV file if it exists.""" From 5cc9b8a02e81e85035e392928a55350393f6ec50 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Mon, 22 Jul 2024 16:53:22 -0400 Subject: [PATCH 8/9] BUG : making sure the cli is in advance --- MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml index 964675e..bfedc32 100644 --- a/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml +++ b/MRI2CBCT_CLI/MRI2CBCT_REG/MRI2CBCT_REG.xml @@ -2,7 +2,7 @@ Automated Dental Tools.Advanced 1 - PRE_ASO_CBCT + MRI2CBCT_REG 0.0.1 https://github.com/username/project From 9595031c49aecd4e276b456842f55e4a9de1af17 Mon Sep 17 00:00:00 2001 From: GaelleLeroux Date: Tue, 23 Jul 2024 16:54:58 -0400 Subject: [PATCH 9/9] BUG : add utils to cmake --- MRI2CBCT/CMakeLists.txt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/MRI2CBCT/CMakeLists.txt b/MRI2CBCT/CMakeLists.txt index f47e2ac..068a372 100644 --- a/MRI2CBCT/CMakeLists.txt +++ b/MRI2CBCT/CMakeLists.txt @@ -4,6 +4,12 @@ set(MODULE_NAME MRI2CBCT) #----------------------------------------------------------------------------- set(MODULE_PYTHON_SCRIPTS ${MODULE_NAME}.py + utils/Method.py + utils/Preprocess_CBCT_MRI.py + utils/Preprocess_CBCT.py + utils/Preprocess_MRI.py + utils/Reg_MRI2CBCT.py + utils/utils_CBCT.py ) set(MODULE_PYTHON_RESOURCES