diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index bcaec63..22dae89 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -5,6 +5,7 @@ import shutil import time import zipfile +from typing import Optional import qt import slicer @@ -379,18 +380,20 @@ def onGeneratePartialVolumes(self): This function creates partial volumes for each segment in the segmentation node for the selected volume node. """ volumeNode = self.ui.volumeSelector.currentNode() - if not volumeNode: - logging.error("Failed to generate partial volume: no volume selected") - return mainOutputDir = self.ui.mainOutputSelector.currentPath - if mainOutputDir is None or mainOutputDir == "": - logging.error("Failed to generate partial volume: no output directory selected") - return - if not os.path.exists(mainOutputDir): - os.makedirs(mainOutputDir) tiffSubDir = self.ui.tiffSubDir.text tfmSubDir = self.ui.tfmSubDir.text - segmentationNode = self.ui.MRMLNodeComboBox.currentNode() + segmentationNode = self.ui.pv_SegNodeComboBox.currentNode() + self.logic.validateInputs( + volumeNode=volumeNode, + segmentationNode=segmentationNode, + mainOutputDir=mainOutputDir, + volumeSubDir=tiffSubDir, + transformSubDir=tfmSubDir, + ) + self.logic.createPathsIfNotExists( + mainOutputDir, os.path.join(mainOutputDir, tiffSubDir), os.path.join(mainOutputDir, tfmSubDir) + ) self.ui.progressBar.setValue(0) self.ui.progressBar.setMaximum(100) self.logic.saveSubVolumesFromSegmentation( @@ -404,119 +407,114 @@ def onGeneratePartialVolumes(self): def onGenerateVRG(self): """ - NOTE - This function is not currently used. It is a work in progress. - This function optimizes the camera positions for a given volume and then generates a VRG file for each optimized camera. """ self.updateProgressBar(0) + # Set up and validate inputs volumeNode = self.ui.volumeSelector.currentNode() - if not volumeNode: - logging.error("Failed to generate VRG: no volume selected") - return mainOutputDir = self.ui.mainOutputSelector.currentPath - if mainOutputDir is None or mainOutputDir == "": - logging.error("Failed to generate VRG: no output directory selected") - return - if not os.path.exists(mainOutputDir): - os.makedirs(mainOutputDir) - + segmentationNode = self.ui.vrg_SegNodeComboBox.currentNode() width = self.ui.vrgRes_width.value height = self.ui.vrgRes_height.value - nPossibleCameras = self.ui.posCamSpin.value nOptimizedCameras = self.ui.optCamSpin.value - + tmpDir = self.ui.vrgTempDir.text + cameraSubDir = self.ui.cameraSubDir.text + vrgSubDir = self.ui.vrgSubDir.text + self.logic.validateInputs( + volumeNode=volumeNode, + segmentationNode=segmentationNode, + mainOutputDir=mainOutputDir, + width=width, + height=height, + nPossibleCameras=nPossibleCameras, + nOptimizedCameras=nOptimizedCameras, + tmpDir=tmpDir, + cameraSubDir=cameraSubDir, + vrgSubDir=vrgSubDir, + ) + self.logic.validatePaths(mainOutputDir=mainOutputDir) if nPossibleCameras < nOptimizedCameras: logging.error("Failed to generate VRG: more optimized cameras than possible cameras") return - newVolumeNode = SubVolumeExtraction.automaticExtraction( - volumeNode, - self.ui.vrgGen_ThresholdSpinBox.value, - segmentationName="Full Extraction", - progressCallback=self.updateProgressBar, - maxProgressValue=10, + # Extract the subvolume for the radiographs + volumeImageData, bounds = self.logic.extractSubVolumeForVRG( + volumeNode, segmentationNode, cameraDebugMode=self.ui.camDebugCheckbox.isChecked() ) - bounds = [0, 0, 0, 0, 0, 0] - newVolumeNode.GetBounds(bounds) - - # rename the volume node - newVolumeNode.SetName(volumeNode.GetName() + " - Bone Subvolume") - self.logic.removeFolderByName(volumeNode.GetName() + " split") # Generate all possible camera positions - logging.info(f"Generating {nPossibleCameras} possible cameras...") camOffset = self.ui.camOffSetSpin.value - cameras = RadiographGeneration.generateNCameras(nPossibleCameras, bounds, camOffset, [width, height]) + cameras = RadiographGeneration.generateNCameras( + nPossibleCameras, bounds, camOffset, [width, height], self.ui.camDebugCheckbox.isChecked() + ) - self.updateProgressBar(13) + self.updateProgressBar(10) # Generate initial VRG for each camera - tmpDir = self.ui.vrgTempDir.text - for i, cam in enumerate(cameras): - logging.info(f"Generating VRG for camera {i}") - cameraDir = os.path.join(mainOutputDir, tmpDir, f"cam{cam.id}") - if not os.path.exists(cameraDir): - os.makedirs(cameraDir) - RadiographGeneration.generateVRG(cam, newVolumeNode, os.path.join(cameraDir, "1.tif"), width, height) - self.updateProgressBar(((i + 1) / nPossibleCameras) * 29 + 13) - - # Want to find the best nOptimizedCameras cameras that have the best DID + self.logic.generateVRGForCameras( + cameras, + volumeImageData, + os.path.join(mainOutputDir, tmpDir), + width, + height, + progressCallback=self.updateProgressBar, + ) + + # Optimize the camera positions bestCameras = RadiographGeneration.optimizeCameras( cameras, os.path.join(mainOutputDir, tmpDir), nOptimizedCameras, progressCallback=self.updateProgressBar ) - # Generate the camera calibration files and VRGs for the best cameras - cameraSubDir = self.ui.cameraSubDir.text - vrgSubDir = self.ui.vrgSubDir.text - for i, cam in enumerate(bestCameras): - logging.info(f"Generating camera calibration file and VRG for camera {i}") - if not os.path.exists(os.path.join(mainOutputDir, cameraSubDir)): - os.makedirs(os.path.join(mainOutputDir, cameraSubDir)) - IO.generateCameraCalibrationFile(cam, os.path.join(mainOutputDir, cameraSubDir, f"cam{cam.id}.yaml")) - cameraDir = os.path.join(mainOutputDir, vrgSubDir, f"cam{cam.id}") - if not os.path.exists(cameraDir): - os.makedirs(cameraDir) - # Copy the VRG to the final directory - shutil.copyfile( - os.path.join(mainOutputDir, tmpDir, f"cam{cam.id}", "1.tif"), os.path.join(cameraDir, "1.tif") - ) - progress = ((i + 1) / nOptimizedCameras) * 29 + 71 - self.updateProgressBar(progress) + # Move the optimized VRGs to the final directory and generate the camera calibration files + self.logic.moveOptimizedVRGsAndGenCalibFiles( + bestCameras, + os.path.join(mainOutputDir, tmpDir), + os.path.join(mainOutputDir, vrgSubDir), + os.path.join(mainOutputDir, cameraSubDir), + progressCallback=self.updateProgressBar, + ) - # remove temp directory + # Clean Up if self.ui.removeVrgTmp.isChecked(): shutil.rmtree(os.path.join(mainOutputDir, tmpDir)) - # remove subvolume node - # slicer.mrmlScene.RemoveNode(newVolumeNode) - def onGenerateConfig(self): """ - NOTE - This function is not currently in use - Generates a complete config file (including all partial volumes, radiographs, and camera calibration files) for Autoscoper. """ volumeNode = self.ui.volumeSelector.currentNode() - if not volumeNode: - logging.error("Failed to generate config: no volume selected") - return mainOutputDir = self.ui.mainOutputSelector.currentPath - if mainOutputDir is None or mainOutputDir == "": - logging.error("Failed to generate config: no output directory selected") - return - if not os.path.exists(mainOutputDir): - os.makedirs(mainOutputDir) - trialName = self.ui.trialName.text - width = self.ui.vrgRes_width.value height = self.ui.vrgRes_height.value + tiffSubDir = self.ui.tiffSubDir.text + vrgSubDir = self.ui.vrgSubDir.text + calibrationSubDir = self.ui.cameraSubDir.text + + # Validate the inputs + self.logic.validateInputs( + volumeNode=volumeNode, + mainOutputDir=mainOutputDir, + trialName=trialName, + width=width, + height=height, + volumeSubDir=tiffSubDir, + vrgSubDir=vrgSubDir, + calibrationSubDir=calibrationSubDir, + ) + self.logic.validatePaths( + mainOutputDir=mainOutputDir, + tiffDir=os.path.join(mainOutputDir, tiffSubDir), + vrgDir=os.path.join(mainOutputDir, vrgSubDir), + calibDir=os.path.join(mainOutputDir, calibrationSubDir), + ) + optimizationOffsets = [ self.ui.optOffX.value, self.ui.optOffY.value, @@ -531,23 +529,7 @@ def onGenerateConfig(self): int(self.ui.flipZ.isChecked()), ] - # Validate the directory structure - tiffSubDir = self.ui.tiffSubDir.text - vrgSubDir = self.ui.vrgSubDir.text - calibrationSubDir = self.ui.cameraSubDir.text - - if not os.path.exists(os.path.join(mainOutputDir, tiffSubDir)): - logging.error(f"Failed to generate config file: {tiffSubDir} not found") - return - if not os.path.exists(os.path.join(mainOutputDir, vrgSubDir)): - logging.error(f"Failed to generate config file: {vrgSubDir} not found") - return - if not os.path.exists(os.path.join(mainOutputDir, calibrationSubDir)): - logging.error(f"Failed to generate config file: {calibrationSubDir} not found") - return - # generate the config file - logging.info("Generating config file") configFilePath = IO.generateConfigFile( mainOutputDir, [tiffSubDir, vrgSubDir, calibrationSubDir], @@ -570,9 +552,7 @@ def onSegmentation(self): volumeNode = self.ui.volumeSelector.currentNode() - if not volumeNode: - logging.error("No volume selected") - return + self.logic.validateInputs(voluemNode=volumeNode) if self.ui.segGen_autoRadioButton.isChecked(): segmentationNode = SubVolumeExtraction.automaticSegmentation( @@ -583,9 +563,7 @@ def onSegmentation(self): ) elif self.ui.segGen_fileRadioButton.isChecked(): segmentationFileDir = self.ui.segGen_lineEdit.currentPath - if not segmentationFileDir or not os.path.exists(segmentationFileDir): - logging.error("No segmentation directory selected") - return + self.logic.validatePaths(segmentationFileDir=segmentationFileDir) segmentationFiles = glob.glob(os.path.join(segmentationFileDir, "*.*")) segmentationNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") segmentationNode.CreateDefaultDisplayNodes() @@ -808,3 +786,218 @@ def showVolumeIn3D(self, volumeNode: slicer.vtkMRMLVolumeNode): volumeNode.AddAndObserveDisplayNodeID(displayNode.GetID()) logic.UpdateDisplayNodeFromVolumeNode(displayNode, volumeNode) slicer.mrmlScene.RemoveNode(slicer.util.getNode("Volume rendering ROI")) + + def validateInputs(self, *args: tuple, **kwargs: dict) -> bool: + """ + Validates that the provided inputs are not None. + + :param args: list of inputs to validate + :type args: tuple + + :param kwargs: list of inputs to validate + :type kwargs: dict + + :return: True if all inputs are valid, False otherwise + :rtype: bool + """ + for arg in args: + if arg is None: + logging.error(f"{arg} is None") + return False + if isinstance(arg, str) and arg == "": + logging.error(f"{arg} is an empty string") + return False + + for name, arg in kwargs.items(): + if arg is None: + logging.error(f"{name} is None") + return False + if isinstance(arg, str) and arg == "": + logging.error(f"{name} is an empty string") + return False + return True + + def validatePaths(self, *args: tuple, **kwargs: dict) -> bool: + """ + Ensures that the provided paths exist. + + :param args: list of paths to validate + :type args: tuple + + :param kwargs: list of paths to validate + :type kwargs: dict + + :return: True if all paths exist, False otherwise + :rtype: bool + """ + for arg in args: + if not os.path.exists(arg): + logging.error(f"{arg} does not exist") + return False + for name, path in kwargs.items(): + if not os.path.exists(path): + logging.error(f"{name} does not exist! \n {path}") + return False + return True + + def createPathsIfNotExists(self, *args: tuple) -> None: + """ + Creates a path if it does not exist. + + :param args: list of paths to create + :type args: tuple + """ + for arg in args: + if not os.path.exists(arg): + os.makedirs(arg) + + def extractSubVolumeForVRG( + self, + volumeNode: slicer.vtkMRMLVolumeNode, + segmentationNode: slicer.vtkMRMLSegmentationNode, + cameraDebugMode: bool = False, + ) -> tuple[vtk.vtkImageData, list[float]]: + """ + Extracts a subvolume from the volumeNode that contains all of the segments in the segmentationNode + + :param volumeNode: volume node + :type volumeNode: slicer.vtkMRMLVolumeNode + :param segmentationNode: segmentation node + :type segmentationNode: slicer.vtkMRMLSegmentationNode + :param cameraDebugMode: Whether or not to keep the extracted volume in the scene, defaults to False + :type cameraDebugMode: bool, optional + + :return: tuple containing the extracted volume and the bounds of the volume + :rtype: tuple[vtk.vtkImageData, list[float]] + """ + mergedSegmentationNode = SubVolumeExtraction.mergeSegments(volumeNode, segmentationNode) + newVolumeNode = SubVolumeExtraction.extractSubVolume( + volumeNode, mergedSegmentationNode, mergedSegmentationNode.GetSegmentation().GetNthSegmentID(0) + ) + newVolumeNode.SetName(volumeNode.GetName() + " - Bone Subvolume") + + bounds = [0, 0, 0, 0, 0, 0] + newVolumeNode.GetBounds(bounds) + + # Copy the metadata from the original volume into the ImageData + newVolumeImageData = vtk.vtkImageData() + newVolumeImageData.DeepCopy(newVolumeNode.GetImageData()) # So we don't modify the original volume + newVolumeImageData.SetSpacing(newVolumeNode.GetSpacing()) + origin = list(newVolumeNode.GetOrigin()) + origin[0:2] = [x * -1 for x in origin[0:2]] + newVolumeImageData.SetOrigin(origin) + + # Ensure we are in the correct orientation (RAS vs LPS) + imageReslice = vtk.vtkImageReslice() + imageReslice.SetInputData(newVolumeImageData) + + axes = vtk.vtkMatrix4x4() + axes.Identity() + axes.SetElement(0, 0, -1) + axes.SetElement(1, 1, -1) + + imageReslice.SetResliceAxes(axes) + imageReslice.Update() + newVolumeImageData = imageReslice.GetOutput() + + if not cameraDebugMode: + slicer.mrmlScene.RemoveNode(newVolumeNode) + slicer.mrmlScene.RemoveNode(mergedSegmentationNode) + + return newVolumeImageData, bounds + + def generateVRGForCameras( + self, + cameras: list[RadiographGeneration.Camera], + volumeImageData: vtk.vtkImageData, + outputDir: str, + width: int, + height: int, + progressCallback=None, + ) -> None: + """ + Generates VRG files for each camera in the cameras list + + :param cameras: list of cameras + :type cameras: list[RadiographGeneration.Camera] + :param volumeImageData: volume image data + :type volumeImageData: vtk.vtkImageData + :param outputDir: output directory + :type outputDir: str + :param width: width of the radiographs + :type width: int + :param height: height of the radiographs + :type height: int + :param progressCallback: progress callback, defaults to None + :type progressCallback: callable, optional + """ + self.createPathsIfNotExists(outputDir) + + if not progressCallback: + logging.warning( + "[AutoscoperM.logic.generateVRGForCameras] " + "No progress callback provided, progress bar will not be updated" + ) + + def progressCallback(x): + return x + + for i, cam in enumerate(cameras): + cameraDir = os.path.join(outputDir, f"cam{cam.id}") + self.createPathsIfNotExists(cameraDir) + RadiographGeneration.generateVRG( + cam, + volumeImageData, + os.path.join( + cameraDir, "1.tif" + ), # When we expand to multiple radiographs, this will need to be updated + width, + height, + ) + progress = ((i + 1) / len(cameras)) * 30 + 10 + progressCallback(progress) + + def moveOptimizedVRGsAndGenCalibFiles( + self, + bestCameras: list[RadiographGeneration.Camera], + tmpDir: str, + finalDir: str, + calibDir: str, + progressCallback: Optional[callable] = None, + ) -> None: + """ + Copies the optimized VRGs from the temporary directory to the final directory + and generates the camera calibration files + + :param bestCameras: list of optimized cameras + :type bestCameras: list[RadiographGeneration.Camera] + :param tmpDir: temporary directory + :type tmpDir: str + :param finalDir: final directory + :type finalDir: str + :param calibDir: calibration directory + :type calibDir: str + :param progressCallback: progress callback, defaults to None + :type progressCallback: callable, optional + """ + self.validatePaths(tmpDir=tmpDir) + self.createPathsIfNotExists(finalDir, calibDir) + if not progressCallback: + logging.warning( + "[AutoscoperM.logic.moveOptimizedVRGsAndGenCalibFiles] " + "No progress callback provided, progress bar will not be updated" + ) + + def progressCallback(x): + return x + + for i, cam in enumerate(bestCameras): + IO.generateCameraCalibrationFile(cam, os.path.join(calibDir, f"cam{cam.id}.yaml")) + cameraDir = os.path.join(finalDir, f"cam{cam.id}") + self.createPathsIfNotExists(cameraDir) + # Copy all tif files from the tmp to the final directory + for file in glob.glob(os.path.join(tmpDir, f"cam{cam.id}", "*.tif")): + shutil.copy(file, cameraDir) + + progress = ((i + 1) / len(bestCameras)) * 10 + 90 + progressCallback(progress) diff --git a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py b/AutoscoperM/AutoscoperMLib/RadiographGeneration.py index 964a5b2..1ec1ecf 100644 --- a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py +++ b/AutoscoperM/AutoscoperMLib/RadiographGeneration.py @@ -11,9 +11,54 @@ def __init__(self) -> None: self.vtkCamera = vtk.vtkCamera() self.imageSize = [512, 512] self.id = -1 + self.FrustumModel = None + def __str__(self) -> str: + dbgStr = f"Camera {self.id}\n" + dbgStr += f"Position: {self.vtkCamera.GetPosition()}\n" + dbgStr += f"Focal Point: {self.vtkCamera.GetFocalPoint()}\n" + dbgStr += f"View Angle: {self.vtkCamera.GetViewAngle()}\n" + dbgStr += f"Clipping Range: {self.vtkCamera.GetClippingRange()}\n" + dbgStr += f"View Up: {self.vtkCamera.GetViewUp()}\n" + dbgStr += f"Direction of Projection: {self.vtkCamera.GetDirectionOfProjection()}\n" + dbgStr += f"Distance: {self.vtkCamera.GetDistance()}\n" + dbgStr += f"Image Size: {self.imageSize}\n" + dbgStr += f"DID: {self.DID}\n" + dbgStr += "~" * 20 + "\n" + return dbgStr -def generateNCameras(N: int, bounds: list[int], offset: int = 100, imageSize: tuple[int] = (512, 512)) -> list[Camera]: + +def _createFrustumModel(cam: Camera) -> None: + planesArray = [0] * 24 + aspectRatio = cam.vtkCamera.GetExplicitAspectRatio() + + cam.vtkCamera.GetFrustumPlanes(aspectRatio, planesArray) + + planes = vtk.vtkPlanes() + planes.SetFrustumPlanes(planesArray) + + hull = vtk.vtkHull() + hull.SetPlanes(planes) + pd = vtk.vtkPolyData() + hull.GenerateHull(pd, [-1000, 1000, -1000, 1000, -1000, 1000]) + + # Display the frustum + model = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + model.SetAndObservePolyData(pd) + model.CreateDefaultDisplayNodes() + model.GetDisplayNode().SetColor(1, 0, 0) + model.GetDisplayNode().SetOpacity(0.3) + # Set display to off + model.GetDisplayNode().SetVisibility(False) + + model.SetName(f"cam{cam.id}-frustum") + + cam.FrustumModel = model + + +def generateNCameras( + N: int, bounds: list[int], offset: int = 100, imageSize: tuple[int] = (512, 512), camDebugMode: bool = False +) -> list[Camera]: """ Generate N cameras @@ -29,6 +74,9 @@ def generateNCameras(N: int, bounds: list[int], offset: int = 100, imageSize: tu :param imageSize: Image size. Defaults to [512,512]. :type imageSize: list[int] + :param camDebugMode: Whether or not to show the cameras in the scene. Defaults to False. + :type camDebugMode: bool + :return: List of cameras :rtype: list[Camera] """ @@ -67,18 +115,38 @@ def generateNCameras(N: int, bounds: list[int], offset: int = 100, imageSize: tu camera = Camera() camera.vtkCamera.SetPosition(points.GetPoint(i)) camera.vtkCamera.SetFocalPoint(center) - camera.vtkCamera.SetViewAngle(25) - camera.vtkCamera.SetClippingRange(0.1, 1000) + camera.vtkCamera.SetViewAngle(30) + # Set the far clipping plane to be the distance from the camera to the far side of the volume + camera.vtkCamera.SetClippingRange(0.1, r + largestDimension) + # camera.vtkCamera.SetClippingRange(0.1, 1000) + camera.vtkCamera.SetViewUp(0, 1, 0) # Set the view up to be the y axis camera.id = i camera.imageSize = imageSize cameras.append(camera) + if camDebugMode: + # Visualize the cameras + markups = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsFiducialNode") + for cam in cameras: + print(cam) + # add a point to the markups node + markups.AddControlPoint( + cam.vtkCamera.GetPosition()[0], + cam.vtkCamera.GetPosition()[1], + cam.vtkCamera.GetPosition()[2], + f"cam{cam.id}", + ) + # lock the point + markups.SetNthControlPointLocked(markups.GetNumberOfControlPoints() - 1, True) + + _createFrustumModel(cam) + return cameras def generateVRG( camera: Camera, - volumeNode: slicer.vtkMRMLVolumeNode, + volumeImageData: vtk.vtkImageData, outputFileName: str, width: int, height: int, @@ -89,8 +157,8 @@ def generateVRG( :param camera: Camera :type camera: Camera - :param volumeNode: Volume node - :type volumeNode: slicer.vtkMRMLVolumeNode + :param volumeImageData: Volume image data + :type volumeImageData: vtk.vtkImageData :param outputFileName: Output file name :type outputFileName: str @@ -105,9 +173,6 @@ def generateVRG( # create the renderer renderer = vtk.vtkRenderer() renderer.SetBackground(1, 1, 1) - renderer.SetUseDepthPeeling(1) - renderer.SetMaximumNumberOfPeels(100) - renderer.SetOcclusionRatio(0.1) # create the render window renderWindow = vtk.vtkRenderWindow() @@ -117,7 +182,7 @@ def generateVRG( # create the volume mapper volumeMapper = vtk.vtkGPUVolumeRayCastMapper() - volumeMapper.SetInputData(volumeNode.GetImageData()) + volumeMapper.SetInputData(volumeImageData) volumeMapper.SetBlendModeToComposite() # create the volume property @@ -129,8 +194,6 @@ def generateVRG( volumeProperty.SetSpecular(0.2) # create a piecewise function for scalar opacity - # first point is X: 300, O: 0.00 - # second point is X: 950, O: 0.20 opacityTransferFunction = vtk.vtkPiecewiseFunction() opacityTransferFunction.AddPoint(-10000, 0.05) opacityTransferFunction.AddPoint(0, 0.00) @@ -145,7 +208,6 @@ def generateVRG( # add the volume to the renderer renderer.AddVolume(volume) renderer.SetActiveCamera(camera.vtkCamera) - renderer.ResetCamera() # render the image renderWindow.Render() @@ -178,6 +240,7 @@ def _calculateDataIntensityDensity(camera: Camera, whiteRadiographFName: str) -> :param whiteRadiographFName: White radiograph file name :type whiteRadiographFName: str """ + import numpy as np import SimpleITK as sitk @@ -259,9 +322,9 @@ def progressCallback(_x): camera = cameras[i] vrgFName = glob.glob(os.path.join(cameraDir, f"cam{camera.id}", "*.tif"))[0] _calculateDataIntensityDensity(camera, vrgFName) - progress = ((i + 1) / len(cameras)) * 29 + 42 + progress = ((i + 1) / len(cameras)) * 50 + 40 progressCallback(progress) - cameras.sort(key=lambda x: x.DID, reverse=True) + cameras.sort(key=lambda x: x.DID) return cameras[:nOptimizedCameras] diff --git a/AutoscoperM/AutoscoperMLib/SubVolumeExtraction.py b/AutoscoperM/AutoscoperMLib/SubVolumeExtraction.py index 3cb50ee..1ce0d78 100644 --- a/AutoscoperM/AutoscoperMLib/SubVolumeExtraction.py +++ b/AutoscoperM/AutoscoperMLib/SubVolumeExtraction.py @@ -5,38 +5,6 @@ import vtk -def automaticExtraction( - volumeNode: slicer.vtkMRMLVolumeNode, - threshold: int, - segmentationName: Optional[str] = None, - progressCallback: Optional[callable] = None, - maxProgressValue: int = 100, -) -> slicer.vtkMRMLVolumeNode: - """ - Automatic extraction of a sub volume node using the threshold value. - - :param volumeNode: Volume node - :type volumeNode: slicer.vtkMRMLVolumeNode - - :param threshold: Threshold value - :type threshold: int - - :param progressCallback: Progress callback. Default is None. - :type progressCallback: callable - - :param maxProgressValue: Maximum progress value. Default is 100. - :type maxProgressValue: int - - :return: SubVolume node - :rtype: slicer.vtkMRMLVolumeNode - """ - segmentationNode = automaticSegmentation( - volumeNode, threshold, segmentationName, progressCallback, maxProgressValue - ) - _mergeSegments(volumeNode, segmentationNode) - return extractSubVolume(volumeNode, segmentationNode) - - def automaticSegmentation( volumeNode: slicer.vtkMRMLVolumeNode, threshold: int, @@ -178,27 +146,52 @@ def extractSubVolume( return _getItemFromFolder(folderName) -def _mergeSegments(volumeNode: slicer.vtkMRMLVolumeNode, segmentationNode: slicer.vtkMRMLSegmentationNode) -> None: +def mergeSegments( + volumeNode: slicer.vtkMRMLVolumeNode, + segmentationNode: slicer.vtkMRMLSegmentationNode, + newSegmentationNode: bool = True, +) -> slicer.vtkMRMLSegmentationNode: """ " - Merges all segments in the segmentation node. + Merges all segments in the segmentation node into one segment. + If newSegmentationNode is True, a new segmentation node is created. + Otherwise the merge is performed in place on the given segmentation node. :param volumeNode: Volume node :type volumeNode: slicer.vtkMRMLVolumeNode :param segmentationNode: Segmentation node. :type segmentationNode: slicer.vtkMRMLSegmentationNode + + :param newSegmentationNode: If True, a new node is created. Otherwise it is performed in place. Default is True. + :type newSegmentationNode: bool + + :return: Segmentation node with the merged segments. If newSegmentationNode is False, None is returned. + :rtype: slicer.vtkMRMLSegmentationNode | None """ + mergeNode = segmentationNode + if newSegmentationNode: + mergeNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentationNode") + mergeNode.CreateDefaultDisplayNodes() # only needed for display + mergeNode.SetReferenceImageGeometryParameterFromVolumeNode(volumeNode) + # copy segments over + for i in range(segmentationNode.GetSegmentation().GetNumberOfSegments()): + segment = segmentationNode.GetSegmentation().GetNthSegment(i) + mergeNode.GetSegmentation().CopySegmentFromSegmentation( + segmentationNode.GetSegmentation(), segment.GetName() + ) + mergeNode.SetName(segmentationNode.GetName() + " merged") + # Create segment editor to get access to effects segmentationEditorWidget = slicer.qMRMLSegmentEditorWidget() segmentationEditorWidget.setMRMLScene(slicer.mrmlScene) segmentEditorNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLSegmentEditorNode") segmentationEditorWidget.setMRMLSegmentEditorNode(segmentEditorNode) - segmentationEditorWidget.setSegmentationNode(segmentationNode) + segmentationEditorWidget.setSegmentationNode(mergeNode) segmentationEditorWidget.setSourceVolumeNode(volumeNode) # Merge Segments inputSegmentIDs = vtk.vtkStringArray() - segmentationNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) + mergeNode.GetDisplayNode().GetVisibleSegmentIDs(inputSegmentIDs) segmentationEditorWidget.setCurrentSegmentID(inputSegmentIDs.GetValue(0)) for i in range(1, inputSegmentIDs.GetNumberOfValues()): @@ -213,12 +206,16 @@ def _mergeSegments(volumeNode: slicer.vtkMRMLVolumeNode, segmentationNode: slice effect.self().onApply() # delete the segment - segmentationNode.GetSegmentation().RemoveSegment(segmentID_to_add) + mergeNode.GetSegmentation().RemoveSegment(segmentID_to_add) # Clean up segmentationEditorWidget = None slicer.mrmlScene.RemoveNode(segmentEditorNode) + if newSegmentationNode: + return mergeNode + return None + def _fillHole(segmentID: str, segmentationEditorWidget: slicer.qMRMLSegmentEditorWidget, marginSize: int) -> None: """ diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index 0994e10..dbcc2dc 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -2,12 +2,15 @@ AutoscoperM + + true + 0 0 1018 - 1240 + 860 @@ -162,7 +165,7 @@ - false + true Trial Name: @@ -172,14 +175,14 @@ - false + true - + - false + true Advanced Options @@ -194,13 +197,6 @@ true - - - - Camera Subdirectory: - - - @@ -211,6 +207,13 @@ + + + + Virtual Radiograph Subdirectory: + + + @@ -218,27 +221,24 @@ - - + + - VRG Resolution: (width,height) + Transforms - - + + - Delete VRG Temp Subdirecory after optimization - - - true + Calibration - - + + - Partial Volume Subdirectory: + Volumes @@ -249,24 +249,47 @@ - - + + - Volumes + VRGs - - + + - Calibration + VRG Resolution: (width,height) - - + + + + true + - VRGs + Delete VRG Temp Subdirecory after optimization + + + true + + + true + + + + + + + Partial Volume Subdirectory: + + + + + + + Partial Volume Transforms Subdirectory: @@ -280,24 +303,20 @@ - - + + - Virtual Radiograph Subdirectory: + Camera Subdirectory: - - + + - Partial Volume Transforms Subdirectory: + Camera Debug Mode - - - - - - Transforms + + false @@ -322,6 +341,9 @@ Segmentation Generation + + false + @@ -403,9 +425,12 @@ Partial Volume Generation + + false + - + true @@ -449,7 +474,7 @@ - false + true VRG Generation @@ -457,6 +482,9 @@ false + + true + @@ -465,6 +493,16 @@ + + + + 10 + + + 50 + + + @@ -472,10 +510,16 @@ - - - - # of Optimized Cameras: + + + + 0 + + + 1000 + + + 400 @@ -491,23 +535,20 @@ 1000 - 100 + 400 Qt::Horizontal - - + + - 0 - - - 1000 + 2 - 100 + 2 @@ -518,37 +559,35 @@ - - - - 10 + + + + # of Optimized Cameras: - - - - 2 - - - 2 + + + + Segmentation Node: - - - 10000 + + + true - - 700 + + + vtkMRMLSegmentationNode + - - - - - - Threshold Value: + + + + + @@ -558,7 +597,7 @@ - false + true Generate Config @@ -566,6 +605,9 @@ false + + true + @@ -774,7 +816,7 @@ AutoscoperM mrmlSceneChanged(vtkMRMLScene*) - MRMLNodeComboBox + pv_SegNodeComboBox setMRMLScene(vtkMRMLScene*) @@ -788,18 +830,18 @@ - camOffSetSlider - valueChanged(int) - camOffSetSpin - setValue(int) + segGen_autoRadioButton + toggled(bool) + segGen_marginSizeSpin + setEnabled(bool) - 553 - 667 + 118 + 310 - 953 - 667 + 594 + 370 @@ -820,50 +862,34 @@ - vrgGen_ThresholdSpinBox + camOffSetSlider valueChanged(int) - segGen_ThresholdSpinBox + camOffSetSpin setValue(int) - 587 + 553 667 - 594 - 346 - - - - - segGen_ThresholdSpinBox - valueChanged(int) - vrgGen_ThresholdSpinBox - setValue(int) - - - 594 - 346 - - - 587 + 953 667 - segGen_autoRadioButton - toggled(bool) - segGen_marginSizeSpin - setEnabled(bool) + AutoscoperM + mrmlSceneChanged(vtkMRMLScene*) + vrg_SegNodeComboBox + setMRMLScene(vtkMRMLScene*) - 118 - 310 + 508 + 619 - 594 - 370 + 587 + 941