diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 80ca66e..ae5d577 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -211,11 +211,15 @@ def setup(self): self.ui.ankleSampleButton.connect("clicked(bool)", lambda: self.onSampleDataButtonClicked("2023-08-01-Ankle")) # Pre-processing Library Buttons + self.ui.volumeSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onCurrentNodeChanged) self.ui.tiffGenButton.connect("clicked(bool)", self.onGeneratePartialVolumes) self.ui.configGenButton.connect("clicked(bool)", self.onGenerateConfig) self.ui.segmentationButton.connect("clicked(bool)", self.onSegmentation) - + self.ui.importModelsButton.connect("clicked(bool)", self.onImportModels) self.ui.loadPVButton.connect("clicked(bool)", self.onLoadPV) + self.ui.populateTrialNameListButton.connect("clicked(bool)", self.onPopulateTrialNameList) + self.ui.populatePartialVolumeListButton.connect("clicked(bool)", self.onPopulatePartialVolumeList) + self.ui.populateCameraCalListButton.connect("clicked(bool)", self.onPopulateCameraCalList) # Default output directory self.ui.mainOutputSelector.setCurrentPath( @@ -225,6 +229,9 @@ def setup(self): # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() + # Trigger any required UI updates based on the volume node selected by default + self.onCurrentNodeChanged() + def cleanup(self): """ Called when the application closes and the module widget is destroyed. @@ -410,6 +417,18 @@ def onSampleDataButtonClicked(self, dataType): for cam in range(numCams): self.logic.AutoscoperSocket.loadFilters(cam, filterSettings) + def onCurrentNodeChanged(self): + """ + Updates and UI components that correspond to the selected input volume node + """ + volumeNode = self.ui.volumeSelector.currentNode() + if volumeNode: + with slicer.util.tryWithErrorDisplay("Failed to grab volume node information", waitCursor=True): + vSizeX, vSizeY, vSizeZ = self.logic.GetVolumeSpacing(volumeNode) + self.ui.voxelSizeX.value = vSizeX + self.ui.voxelSizeY.value = vSizeY + self.ui.voxelSizeZ.value = vSizeZ + def onGeneratePartialVolumes(self): """ This function creates partial volumes for each segment in the segmentation node for the selected volume node. @@ -503,13 +522,19 @@ def onGenerateConfig(self): self.ui.optOffPitch.value, self.ui.optOffRoll.value, ] + volumeFlip = [ int(self.ui.flipX.isChecked()), int(self.ui.flipY.isChecked()), int(self.ui.flipZ.isChecked()), ] - voxel_spacing = self.logic.GetVolumeSpacing(volumeNode) + voxel_spacing = [ + self.ui.voxelSizeX.value, + self.ui.voxelSizeY.value, + self.ui.voxelSizeZ.value, + ] + # generate the config file configFilePath = IO.generateConfigFile( mainOutputDir, @@ -524,9 +549,44 @@ def onGenerateConfig(self): self.ui.configSelector.setCurrentPath(configFilePath) slicer.util.messageBox("Success!") + def onImportModels(self): + """ + Imports Models from a directory- converts to Segmentation Nodes + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): + self.ui.progressBar.setValue(0) + self.ui.progressBar.setMaximum(100) + + volumeNode = self.ui.volumeSelector.currentNode() + + if not self.logic.validateInputs(voluemNode=volumeNode): + raise ValueError("Invalid inputs") + return + + if self.ui.segGen_fileRadioButton.isChecked(): + segmentationFileDir = self.ui.segGen_lineEdit.currentPath + if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir): + raise ValueError("Invalid paths") + return + segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*")) + segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + segmentationNode.CreateDefaultDisplayNodes() + for idx, file in enumerate(segmentationFiles): + returnedNode = IO.loadSegmentation(segmentationNode, file) + if returnedNode: + # get the segment from the returned node and add it to the segmentation node + segment = returnedNode.GetSegmentation().GetNthSegment(0) + segmentationNode.GetSegmentation().AddSegment(segment) + slicer.mrmlScene.RemoveNode(returnedNode) + self.ui.progressBar.setValue((idx + 1) / len(segmentationFiles) * 100) + else: # Should never happen but just in case + raise Exception("Please select the 'Segmentation From Model' option in order to import models") + return + slicer.util.messageBox("Success!") + def onSegmentation(self): """ - Either launches the automatic segmentation process or loads in a set of segmentations from a directory + Launches the automatic segmentation process """ with slicer.util.tryWithErrorDisplay("Failed to compute results", waitCursor=True): self.ui.progressBar.setValue(0) @@ -561,24 +621,8 @@ def onSegmentation(self): segmentationSequenceNode.SetDataNodeAtValue(segmentationNode, str(i)) slicer.mrmlScene.RemoveNode(segmentationNode) currentVolumeNode = self.logic.getNextItemInSequence(volumeNode) - elif self.ui.segGen_fileRadioButton.isChecked(): - segmentationFileDir = self.ui.segGen_lineEdit.currentPath - if not self.logic.validatePaths(segmentationFileDir=segmentationFileDir): - raise ValueError("Invalid paths") - return - segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*")) - segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") - segmentationNode.CreateDefaultDisplayNodes() - for idx, file in enumerate(segmentationFiles): - returnedNode = IO.loadSegmentation(segmentationNode, file) - if returnedNode: - # get the segment from the returned node and add it to the segmentation node - segment = returnedNode.GetSegmentation().GetNthSegment(0) - segmentationNode.GetSegmentation().AddSegment(segment) - slicer.mrmlScene.RemoveNode(returnedNode) - self.ui.progressBar.setValue((idx + 1) / len(segmentationFiles) * 100) else: # Should never happen but just in case - raise Exception("No segmentation method selected") + raise Exception("Please select the 'Automatic Segmentation' option in order to generate segmentations") return slicer.util.messageBox("Success!") @@ -649,6 +693,63 @@ def onLoadPV(self): slicer.util.messageBox("Success!") + def onPopulateTrialNameList(self): + """ + Populates trial name UI list using files from the selected radiograph directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.trialList, self.ui.vrgSubDir.text, itemType="dir") + + def onPopulatePartialVolumeList(self): + """ + Populates partial volumes UI list using files from the selected PV directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.partialVolumeList, self.ui.tiffSubDir.text) + + def onPopulateCameraCalList(self): + """ + Populates camera calibration UI list using files from the selected camera directory + """ + with slicer.util.tryWithErrorDisplay("Failed to compute results.", waitCursor=True): + self.populateListFromOutputSubDir(self.ui.camCalList, self.ui.cameraSubDir.text) + + def populateListFromOutputSubDir(self, listWidget, fileSubDir, itemType="file"): + """ + Populates input UI list with files/directories that exist in the given input directory + """ + listWidget.clear() + + mainOutputDir = self.ui.mainOutputSelector.currentPath + if not self.logic.validateInputs( + listWidget=listWidget, + mainOutputDir=mainOutputDir, + fileSubDir=fileSubDir, + ): + raise ValueError(f"Invalid inputs") + return + + fileDir = os.path.join(mainOutputDir, fileSubDir) + if not self.logic.validatePaths(fileDir=fileDir): + raise ValueError(f"Invalid input: subdirectory '{fileDir}' does not exist.") + return + + if itemType == "file": + listFiles = [f.name for f in os.scandir(fileDir) if os.path.isfile(f)] + elif itemType == "dir": + listFiles = [f.name for f in os.scandir(fileDir) if os.path.isdir(f)] + else: + raise ValueError( + "Invalid input: can either search for type 'file' or 'dir' " + f"in specified path, but given itemType='{itemType}'" + ) + return + + for file in sorted(listFiles): + fileItem = qt.QListWidgetItem(file) + fileItem.setFlags(fileItem.flags() & ~qt.Qt.ItemIsSelectable) # Remove the selectable flag + fileItem.setCheckState(qt.Qt.Unchecked) + listWidget.addItem(fileItem) # # AutoscoperMLogic diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index 09d347a..dd0eca6 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -9,8 +9,8 @@ 0 0 - 1018 - 860 + 811 + 1566 @@ -120,6 +120,13 @@ + + + + Qt::Vertical + + + @@ -128,235 +135,71 @@ - - - General Input + + + 0 - - true + + false - - - - - Volume Node: - - - - - - - false - - - - vtkMRMLScalarVolumeNode - vtkMRMLSequenceNode - - - - - - - - - - - - - - Output Directory: - - - - - - - ctkPathLineEdit::Dirs|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Writable - - - - - - - true - - - Trial Name: - - - - - - - true - - - - - - - true - - - Advanced Options - - - true - - - true - - - false - - - - - - Camera Subdirectory: - - - - - - - Radiograph Subdirectory: - - - - - - - true - - - Calibration - - - - - - - Tracking Subdirectory: - - - - - - - VRG Resolution: (width,height) - - - - - - - Transforms - - - - - - - Volumes - - - - - - - 999999999 - - - 1760 - - - - - - - Partial Volume Transforms Subdirectory: - - - - - - - RadiographImages - - - - - - - 999999999 - - - 1760 - - - - - - - Partial Volume Subdirectory: - - - - - - - Model Subdirectory: - - - - - - - Tracking - - - - - - - Models - - - - - - - - - - 0 - - - false - - - - + + + + + + Output Directory: + + + + + + + ctkPathLineEdit::Dirs|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Writable + + + + + + + Volume Node: + + + + + + + true + + + + vtkMRMLScalarVolumeNode + vtkMRMLSequenceNode + + + + + + + + + + + + Segmentation Generation + true + + false - - - - Threshold Value: - - - @@ -368,23 +211,13 @@ - + - Batch Load from File - - - - - - - false - - - ctkPathLineEdit::Dirs|ctkPathLineEdit::Drives|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Readable + Threshold Value: - + 10000 @@ -394,31 +227,68 @@ - - + + - Segmentation File Directory: + Margin Size: - + + + + 2.000000000000000 + + + + Generate Segmentations + + + + OR + + + - + - Margin Size: + Segmentation from Model - - - 2.000000000000000 + + + false + + + STL Models Directory: + + + + + + + false + + + ctkPathLineEdit::Dirs|ctkPathLineEdit::Drives|ctkPathLineEdit::Executable|ctkPathLineEdit::NoDot|ctkPathLineEdit::NoDotDot|ctkPathLineEdit::Readable + + + + + + + false + + + Import Models @@ -431,9 +301,19 @@ Partial Volume Generation + true + + false + + + + Segmentation Node: + + + @@ -452,13 +332,6 @@ - - - - Segmentation Node: - - - @@ -477,148 +350,430 @@ - + true - Generate Config + Default Subdirectories + + + true - false + true true - - - + + + - Flip Y - - - false + Partial Volume Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Volumes - - + + - Generate Config File + Partial Volume Transforms Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Transforms - - - - Flip X + + + + + 75 + true + - - false + + Radiograph Subdirectory: - - - - 0.100000000000000 + + + + + 75 + true + - - 0.100000000000000 + + RadiographImages - - - - 0.100000000000000 + + + + + 75 + true + - - 0.100000000000000 + + Camera Subdirectory: - - + + + + true + + + + 75 + true + + - Flip Z + Calibration - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Tracking Subdirectory: - - - - 0.100000000000000 - - - 0.100000000000000 + + + + Tracking - - + + - Optimization Offsets: + Model Subdirectory: - - + + - Volume Flip: + Models + + + + true + + + Generate Config + + + true + + + false + + + + + + + + true + + + Select Trial Names: + + + + + + + Populate From Radiographs Subdirectory + + + + + + + + + + and enter : + + + + + + + true + + + + + + + .cfg + + + + + + + Select Partial Volumes: + + + + + + + Populate From Volume Subdirectory + + + + + + + + + + Select Camera Calibrations: + + + + + + + Populate From Camera Subdirectory + + + + + + + + + + + + + + Optimization Offsets: + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + 0.100000000000000 + + + 0.100000000000000 + + + + + + + Volume Flip: + + + + + + + Flip X + + + false + + + + + + + Flip Y + + + false + + + + + + + Flip Z + + + + + + + Render Resolution: (width,height) + + + + + + + 999999999 + + + 1760 + + + + + + + 999999999 + + + 1760 + + + + + + + Voxel Size: + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + true + + + 3 + + + 1.000000000000000 + + + + + + + Generate Config File + + + + + + + + + + + + Qt::Vertical + + + - - - - Qt::Vertical - - - - 20 - 40 - - - - @@ -639,17 +794,6 @@
ctkCollapsibleButton.h
1 - - ctkCollapsibleGroupBox - QGroupBox -
ctkCollapsibleGroupBox.h
- 1 -
- - ctkDoubleRangeSlider - QWidget -
ctkDoubleRangeSlider.h
-
ctkPathLineEdit QWidget @@ -738,5 +882,85 @@ + + segGen_fileRadioButton + toggled(bool) + importModelsButton + setEnabled(bool) + + + 122 + 276 + + + 698 + 204 + + + + + segGen_autoRadioButton + toggled(bool) + segmentationButton + setEnabled(bool) + + + 122 + 189 + + + 698 + 276 + + + + + segGen_fileRadioButton + toggled(bool) + segSTLModelsDirLabel + setEnabled(bool) + + + 122 + 276 + + + 279 + 276 + + + + + segGen_autoRadioButton + toggled(bool) + segThresholdLabel + setEnabled(bool) + + + 122 + 189 + + + 279 + 189 + + + + + segGen_autoRadioButton + toggled(bool) + segMarginSizeLabel + setEnabled(bool) + + + 122 + 189 + + + 279 + 220 + + +