From 713e827bd4178d183105fde62d72a1149c914a22 Mon Sep 17 00:00:00 2001 From: Anthony Lombardi Date: Fri, 25 Aug 2023 10:20:19 -0400 Subject: [PATCH] ENH: Manual camera placement --- AutoscoperM/AutoscoperM.py | 96 +++++ AutoscoperM/AutoscoperMLib/IO.py | 9 + .../AutoscoperMLib/RadiographGeneration.py | 81 +++- AutoscoperM/Resources/UI/AutoscoperM.ui | 373 ++++++++++++++---- 4 files changed, 461 insertions(+), 98 deletions(-) diff --git a/AutoscoperM/AutoscoperM.py b/AutoscoperM/AutoscoperM.py index 6bcb5fc..dab2379 100644 --- a/AutoscoperM/AutoscoperM.py +++ b/AutoscoperM/AutoscoperM.py @@ -195,6 +195,7 @@ def setup(self): # Pre-processing Library Buttons self.ui.tiffGenButton.connect("clicked(bool)", self.onGeneratePartialVolumes) self.ui.vrgGenButton.connect("clicked(bool)", self.onGenerateVRG) + self.ui.manualVRGGenButton.connect("clicked(bool)", self.onManualVRGGen) self.ui.configGenButton.connect("clicked(bool)", self.onGenerateConfig) self.ui.segmentationButton.connect("clicked(bool)", self.onSegmentation) @@ -205,6 +206,11 @@ def setup(self): os.path.join(slicer.mrmlScene.GetCacheManager().GetRemoteCacheDirectory(), "AutoscoperM-Pre-Processing") ) + # Dynamic camera frustum functions + self.ui.mVRG_markupSelector.connect("currentNodeChanged(vtkMRMLNode*)", self.onMarkupNodeChanged) + self.ui.mVRG_ClippingRangeSlider.connect("valuesChanged(double,double)", self.updateClippingRange) + self.ui.mVRG_viewAngleSpin.connect("valueChanged(int)", self.updateViewAngle) + # Make sure parameter node is initialized (needed for module reload) self.initializeParameterNode() @@ -609,6 +615,95 @@ def onLoadPV(self): volumeNode.SetAndObserveTransformNodeID(transformNode.GetID()) self.logic.showVolumeIn3D(volumeNode) + def onManualVRGGen(self): + markupsNode = self.ui.mVRG_markupSelector.currentNode() + volumeNode = self.ui.volumeSelector.currentNode() + segmentationNode = self.ui.mVRG_segmentationSelector.currentNode() + mainOutputDir = self.ui.mainOutputSelector.currentPath + viewAngle = self.ui.mVRG_viewAngleSpin.value + clippingRange = (self.ui.mVRG_ClippingRangeSlider.minimumValue, self.ui.mVRG_ClippingRangeSlider.maximumValue) + width = self.ui.vrgRes_width.value + height = self.ui.vrgRes_height.value + vrgDir = self.ui.vrgSubDir.text + cameraDir = self.ui.cameraSubDir.text + if not self.logic.validateInputs( + markupsNode=markupsNode, + volumeNode=volumeNode, + segmentationNode=segmentationNode, + mainOutputDir=mainOutputDir, + viewAngle=viewAngle, + clippingRange=clippingRange, + width=width, + height=height, + vrgDir=vrgDir, + cameraDir=cameraDir, + ): + logging.error("Failed to generate VRG: invalid inputs") + return + if not self.logic.validatePaths(mainOutputDir=mainOutputDir): + logging.error("Failed to generate VRG: invalid output directory") + return + self.logic.createPathsIfNotExists(os.path.join(mainOutputDir, vrgDir), os.path.join(mainOutputDir, cameraDir)) + + if self.logic.vrgManualCameras is None: + self.onMarkupNodeChanged(markupsNode) # create the cameras + + volumeImageData, _ = self.logic.extractSubVolumeForVRG( + volumeNode, segmentationNode, cameraDebugMode=self.ui.camDebugCheckbox.isChecked() + ) + + self.logic.generateVRGForCameras( + self.logic.vrgManualCameras, + volumeImageData, + os.path.join(mainOutputDir, vrgDir), + width, + height, + progressCallback=self.updateProgressBar, + ) + + self.updateProgressBar(100) + + for cam in self.logic.vrgManualCameras: + IO.generateCameraCalibrationFile(cam, os.path.join(mainOutputDir, cameraDir, f"cam{cam.id}.yaml")) + + def onMarkupNodeChanged(self, node): + if node is None: + if self.logic.vrgManualCameras is not None: + # clean up + for cam in self.logic.vrgManualCameras: + slicer.mrmlScene.RemoveNode(cam.FrustumModel) + self.logic.vrgManualCameras = None + return + if self.logic.vrgManualCameras is not None: + # clean up + for cam in self.logic.vrgManualCameras: + slicer.mrmlScene.RemoveNode(cam.FrustumModel) + self.logic.vrgManualCameras = None + # get the volume and segmentation nodes + segmentationNode = self.ui.mVRG_segmentationSelector.currentNode() + if not self.logic.validateInputs(segmentationNode=segmentationNode): + return + bounds = [0] * 6 + segmentationNode.GetBounds(bounds) + self.logic.vrgManualCameras = RadiographGeneration.generateCamerasFromMarkups( + node, + bounds, + (self.ui.mVRG_ClippingRangeSlider.minimumValue, self.ui.mVRG_ClippingRangeSlider.maximumValue), + self.ui.mVRG_viewAngleSpin.value, + [self.ui.vrgRes_width.value, self.ui.vrgRes_height.value], + True, + ) + + def updateClippingRange(self, min, max): + for cam in self.logic.vrgManualCameras: + cam.vtkCamera.SetClippingRange(min, max) + RadiographGeneration._updateFrustumModel(cam) + + def updateViewAngle(self, value): + for cam in self.logic.vrgManualCameras: + cam.vtkCamera.SetViewAngle(value) + RadiographGeneration._updateFrustumModel(cam) + # # AutoscoperMLogic @@ -634,6 +729,7 @@ def __init__(self): self.AutoscoperProcess = qt.QProcess() self.AutoscoperProcess.setProcessChannelMode(qt.QProcess.ForwardedChannels) self.AutoscoperSocket = None + self.vrgManualCameras = None def setDefaultParameters(self, parameterNode): """ diff --git a/AutoscoperM/AutoscoperMLib/IO.py b/AutoscoperM/AutoscoperMLib/IO.py index 3357508..ccf5733 100644 --- a/AutoscoperM/AutoscoperMLib/IO.py +++ b/AutoscoperM/AutoscoperMLib/IO.py @@ -74,6 +74,15 @@ def generateCameraCalibrationFile(camera: Camera, filename: str): + str(camera.vtkCamera.GetPosition()[2]) + "]\n" ) + f.write( + "view-up: [" + + str(camera.vtkCamera.GetViewUp()[0]) + + ", " + + str(camera.vtkCamera.GetViewUp()[1]) + + ", " + + str(camera.vtkCamera.GetViewUp()[2]) + + "]\n" + ) f.write("view-angle: " + str(camera.vtkCamera.GetViewAngle()) + "\n") f.write("image-width: " + str(camera.imageSize[0]) + "\n") f.write("image-height: " + str(camera.imageSize[1]) + "\n") diff --git a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py b/AutoscoperM/AutoscoperMLib/RadiographGeneration.py index 69cd98e..6b4bbfa 100644 --- a/AutoscoperM/AutoscoperMLib/RadiographGeneration.py +++ b/AutoscoperM/AutoscoperMLib/RadiographGeneration.py @@ -29,6 +29,23 @@ def __str__(self) -> str: def _createFrustumModel(cam: Camera) -> None: + model = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode") + model.CreateDefaultDisplayNodes() + model.GetDisplayNode().SetColor(1, 0, 0) + model.GetDisplayNode().SetOpacity(0.3) + model.GetDisplayNode().SetVisibility(True) + + model.SetName(f"cam{cam.id}-frustum") + + cam.FrustumModel = model + + _updateFrustumModel(cam) + + +def _updateFrustumModel(cam: Camera) -> None: + if cam.FrustumModel is None: + _createFrustumModel(cam) + return planesArray = [0] * 24 aspectRatio = cam.vtkCamera.GetExplicitAspectRatio() @@ -42,18 +59,7 @@ def _createFrustumModel(cam: Camera) -> None: 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 + cam.FrustumModel.SetAndObservePolyData(pd) def generateNCameras( @@ -116,10 +122,8 @@ def generateNCameras( camera.vtkCamera.SetPosition(points.GetPoint(i)) camera.vtkCamera.SetFocalPoint(center) 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.vtkCamera.SetViewUp(0, 1, 0) camera.id = i camera.imageSize = imageSize cameras.append(camera) @@ -144,6 +148,53 @@ def generateNCameras( return cameras +def generateCamerasFromMarkups( + fiduaicalNode: slicer.vtkMRMLMarkupsFiducialNode, + volumeBounds: list[int], + clippingRange: tuple[int], + viewAngle: int, + imageSize: tuple[int] = (512, 512), + cameraDebug: bool = False, +) -> list[Camera]: + """ + Generate cameras from a markups fiducial node + + :param fiduaicalNode: Markups fiducial node + :type fiduaicalNode: slicer.vtkMRMLMarkupsFiducialNode + :param volumeBounds: Bounds of the volume + :type volumeBounds: list[int] + :param clippingRange: Clipping range + :type clippingRange: tuple[int] + :param viewAngle: View angle + :type viewAngle: int + :param imageSize: Image size. Defaults to [512,512]. + :type imageSize: list[int] + :param cameraDebug: Whether or not to show the cameras in the scene. Defaults to False. + :type cameraDebug: bool + :return: List of cameras + """ + center = [ + (volumeBounds[0] + volumeBounds[1]) / 2, + (volumeBounds[2] + volumeBounds[3]) / 2, + (volumeBounds[4] + volumeBounds[5]) / 2, + ] + n = fiduaicalNode.GetNumberOfControlPoints() + cameras = [] + for i in range(n): + camera = Camera() + camera.vtkCamera.SetPosition(fiduaicalNode.GetNthControlPointPosition(i)) + camera.vtkCamera.SetFocalPoint(center) + camera.vtkCamera.SetViewAngle(viewAngle) + camera.vtkCamera.SetClippingRange(clippingRange[0], clippingRange[1]) + camera.vtkCamera.SetViewUp(0, 1, 0) + camera.id = fiduaicalNode.GetNthControlPointLabel(i) + camera.imageSize = imageSize + if cameraDebug: + _createFrustumModel(camera) + cameras.append(camera) + return cameras + + def generateVRG( camera: Camera, volumeImageData: vtk.vtkImageData, diff --git a/AutoscoperM/Resources/UI/AutoscoperM.ui b/AutoscoperM/Resources/UI/AutoscoperM.ui index dbcc2dc..12d7ac9 100644 --- a/AutoscoperM/Resources/UI/AutoscoperM.ui +++ b/AutoscoperM/Resources/UI/AutoscoperM.ui @@ -269,7 +269,7 @@ true - Delete VRG Temp Subdirecory after optimization + Delete Temporary VRG Files true @@ -421,16 +421,99 @@ - + - Partial Volume Generation + VRG Generation - Manual Camera Placement false - - - + + + + + View Angle + + + + + + + 0.100000000000000 + + + 2000.000000000000000 + + + 0.100000000000000 + + + 0.100000000000000 + + + 300.000000000000000 + + + Qt::Horizontal + + + + + + + Segmentation Node + + + + + + + Camera Positions + + + + + + + 0.100000000000000 + + + 2000.000000000000000 + + + + + + + Clipping Range + + + + + + + 0.100000000000000 + + + 2000.000000000000000 + + + 300.000000000000000 + + + + + + + 360 + + + 30 + + + + + true @@ -447,24 +530,28 @@ - - - - Segmentation Node: + + + + true - - - - - - Generate Partial Volumes + + + vtkMRMLMarkupsFiducialNode + + + + + + + - - + + - Load Partial Volumes + Generate VRGs from Markups @@ -477,7 +564,7 @@ true - VRG Generation + VRG Generation - Automatic Camera Placement false @@ -486,14 +573,20 @@ true - - - - Generate VRGs + + + + 0 + + + 1000 + + + 400 - + 10 @@ -510,20 +603,32 @@ - - - - 0 + + + + # of Possible Cameras: - - 1000 + + + + + + true - - 400 + + + vtkMRMLSegmentationNode + + + + + + + - + false @@ -542,20 +647,17 @@ - - - - 2 - - - 2 + + + + Generate VRGs - - + + - # of Possible Cameras: + Segmentation Node: @@ -566,15 +668,30 @@ - - - - Segmentation Node: + + + + 2 + + + 2 - - + + + + + + + Partial Volume Generation + + + false + + + + true @@ -591,6 +708,27 @@ + + + + Segmentation Node: + + + + + + + Generate Partial Volumes + + + + + + + Load Partial Volumes + + + @@ -609,15 +747,15 @@ true - - + + - Optimization Offsets: + Flip Y - - + + 0.100000000000000 @@ -633,8 +771,8 @@ - - + + 0.100000000000000 @@ -643,8 +781,15 @@ - - + + + + Flip X + + + + + 0.100000000000000 @@ -653,8 +798,8 @@ - - + + 0.100000000000000 @@ -663,6 +808,13 @@ + + + + Flip Z + + + @@ -673,8 +825,8 @@ - - + + 0.100000000000000 @@ -683,31 +835,17 @@ - - - - Volume Flip: - - - - - - - Flip X - - - - - + + - Flip Y + Optimization Offsets: - - + + - Flip Z + Volume Flip: @@ -757,6 +895,11 @@
ctkCollapsibleGroupBox.h
1 + + ctkDoubleRangeSlider + QWidget +
ctkDoubleRangeSlider.h
+
ctkPathLineEdit QWidget @@ -893,5 +1036,69 @@ + + AutoscoperM + mrmlSceneChanged(vtkMRMLScene*) + mVRG_markupSelector + setMRMLScene(vtkMRMLScene*) + + + 508 + 429 + + + 507 + 409 + + + + + AutoscoperM + mrmlSceneChanged(vtkMRMLScene*) + mVRG_segmentationSelector + setMRMLScene(vtkMRMLScene*) + + + 508 + 429 + + + 507 + 409 + + + + + mVRG_ClippingRangeSlider + minimumValueChanged(double) + mVRG_clippingRangeMinBox + setValue(double) + + + 567 + 499 + + + 189 + 499 + + + + + mVRG_ClippingRangeSlider + maximumValueChanged(double) + mVRG_clippingRangeMaxBox + setValue(double) + + + 567 + 499 + + + 944 + 499 + + +