From e89a735e9fda3bec889143b2d9c9ec4839234f19 Mon Sep 17 00:00:00 2001 From: Anthony Lombardi Date: Thu, 25 Jul 2024 11:53:25 -0400 Subject: [PATCH] ENH: Export AUT_{bone}.stl during PV gen --- AutoscoperM/AutoscoperM.py | 48 +++++++- AutoscoperM/Resources/UI/AutoscoperM.ui | 144 ++++++++++++++---------- 2 files changed, 132 insertions(+), 60 deletions(-) diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 694f9b4..4e39d16 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -441,6 +441,8 @@ def onGeneratePartialVolumes(self): mainOutputDir = self.ui.mainOutputSelector.currentPath tiffSubDir = self.ui.tiffSubDir.text tfmSubDir = self.ui.tfmSubDir.text + trackingSubDir = self.ui.trackingSubDir.text + modelSubDir = self.ui.modelSubDir.text segmentationNode = self.ui.pv_SegNodeComboBox.currentNode() if not self.logic.validateInputs( volumeNode=volumeNode, @@ -448,12 +450,18 @@ def onGeneratePartialVolumes(self): mainOutputDir=mainOutputDir, volumeSubDir=tiffSubDir, transformSubDir=tfmSubDir, + trackingSubDir=trackingSubDir, + modelSubDir=modelSubDir, ): raise ValueError("Invalid inputs") return self.logic.createPathsIfNotExists( - mainOutputDir, os.path.join(mainOutputDir, tiffSubDir), os.path.join(mainOutputDir, tfmSubDir) + mainOutputDir, + os.path.join(mainOutputDir, tiffSubDir), + os.path.join(mainOutputDir, tfmSubDir), + os.path.join(mainOutputDir, trackingSubDir), + os.path.join(mainOutputDir, modelSubDir), ) self.ui.progressBar.setValue(0) self.ui.progressBar.setMaximum(100) @@ -463,6 +471,8 @@ def onGeneratePartialVolumes(self): mainOutputDir, volumeSubDir=tiffSubDir, transformSubDir=tfmSubDir, + trackingSubDir=trackingSubDir, + modelSubDir=modelSubDir, progressCallback=self.updateProgressBar, ) slicer.util.messageBox("Success!") @@ -1013,6 +1023,7 @@ def saveSubVolumesFromSegmentation( volumeSubDir: str = "Volumes", transformSubDir: str = "Transforms", trackingSubDir: str = "Tracking", + modelSubDir: str = "Models", progressCallback: Optional[callable] = None, ) -> bool: """ @@ -1065,7 +1076,6 @@ def progressCallback(x): segmentVolume.DisableModifiedEventOff() transformFilenameBase = os.path.join(outputDir, transformSubDir, segmentName) - origin = segmentVolume.GetOrigin() IO.writeTFMFile(f"{transformFilenameBase}_t.tfm", [1.0, 1.0, 1.0], origin) spacing = segmentVolume.GetSpacing() @@ -1082,6 +1092,10 @@ def progressCallback(x): dicom2autNode = self.createAndAddDicom2AutTransformNode(origin, pvol2autNode) dicom2autFilename = os.path.join(outputDir, transformSubDir, f"{segmentVolume.GetName()}-DICOM2AUT.tfm") slicer.util.exportNode(dicom2autNode, dicom2autFilename) + + stlFilename = os.path.join(outputDir, modelSubDir, f"AUT_{segmentVolume.GetName()}.stl") + self.exportSTLFromSegment(segmentationNode, segmentID, stlFilename, dicom2autNode.GetTransformToParent()) + slicer.mrmlScene.RemoveNode(dicom2autNode) # Create TRA file @@ -1122,6 +1136,36 @@ def progressCallback(x): slicer.app.layoutManager().resetSliceViews() return True + @staticmethod + def exportSTLFromSegment( + segmentationNode: slicer.vtkMRMLSegmentationNode, + segmentID: str, + filename: str, + transform: Optional[vtk.vtkAbstractTransform] = None, + ): + """Utility functions for exporting a segment as an STL. Optionally takes a vtk transform.""" + if not segmentationNode.CreateClosedSurfaceRepresentation(): + raise ValueError( + f"Failed to generate the closed surface representation from segmentation: {segmentationNode.GetName()}" + ) + + polyData = vtk.vtkPolyData() + if not segmentationNode.GetClosedSurfaceRepresentation(segmentID, polyData): + raise ValueError(f"Failed to get PolyData for segmentationNode {segmentationNode.GetName()}") + + if transform is not None: + transformFilter = vtk.vtkTransformPolyDataFilter() + transformFilter.SetInputData(polyData) + transformFilter.SetTransform(transform) + transformFilter.Update() + polyData = transformFilter.GetOutput() + + stlNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + stlNode.SetAndObservePolyData(polyData) + + slicer.util.exportNode(stlNode, filename) + slicer.mrmlScene.RemoveNode(stlNode) + @staticmethod def showVolumeIn3D(volumeNode: slicer.vtkMRMLVolumeNode): logic = slicer.modules.volumerendering.logic() diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index 2cc142c..19e9e66 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -17,7 +17,7 @@ - 0 + 1 @@ -205,10 +205,10 @@ true - false + true - true + false @@ -218,83 +218,93 @@ - - + + + + true + - Partial Volume Transforms Subdirectory: + Delete Temporary VRG Files + + + true + + + true - - + + - Virtual Radiograph Subdirectory: + Only use indices for radiograph filename - - + + - Volumes + VRG Temp Subdirectory: - - + + - VRG Resolution: (width,height) + Virtual Radiograph Subdirectory: - - + + + + true + - VRG-Temp + Calibration - - + + - RadiographImages + Tracking Subdirectory: - - - - false - + + - Delete Temporary VRG Files - - - true - - - true + VRG Resolution: (width,height) - - + + - VRG Temp Subdirectory: + Transforms - - - + + + + Camera Debug Mode + + false + + + + - Calibration + Volumes - - + + 999999999 @@ -303,15 +313,22 @@ - - + + - Partial Volume Subdirectory: + Partial Volume Transforms Subdirectory: - - + + + + RadiographImages + + + + + 999999999 @@ -320,27 +337,38 @@ - - + + - Transforms + VRG-Temp - - + + - Camera Debug Mode + Partial Volume Subdirectory: - - false + + + + + + Model Subdirectory: - - + + - Only use indices for radiograph filename + Tracking + + + + + + + Models