Skip to content

Commit

Permalink
ENH: Improve Endoscopy flythrough
Browse files Browse the repository at this point in the history
  • Loading branch information
Leengit committed Nov 3, 2023
1 parent af8034f commit 39cfc8a
Showing 1 changed file with 73 additions and 52 deletions.
125 changes: 73 additions & 52 deletions Modules/Scripted/Endoscopy/Endoscopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@ def setup(self):
self.outputPathNodeSelector.setMRMLScene(slicer.mrmlScene)

# In the Endoscopy module we want mouse involvement in the 3-dimensional view to rotate the camera orientation.
# How do we ensure that is enabled? How do we disable all other uses of the mouse in that pane.
# TODO: Is that what we really want? Is this the way to do that or need we do more, less, different?
# TODO: How do we ensure that is enabled? How do we disable all other uses of the mouse in that pane? Is that
# what we really want? Is this the way to do that or need we do more, less, different?
interactionNode = slicer.mrmlScene.GetNodeByID("vtkMRMLInteractionNodeSingleton")
interactionNode.SwitchToViewTransformMode()
interactionNode.SetPlaceModePersistence(0)
Expand Down Expand Up @@ -341,8 +341,9 @@ def setFiducialNode(self, newFiducialNode):
)

if self.logic:
# We are going to have rebuild the EndoscopyLogic later
self.logic.cleanup()
self.logic = EndoscopyLogic(newFiducialNode)
self.logic = None
else:
self.flythroughCollapsibleButton.enabled = False

Expand All @@ -352,16 +353,9 @@ def setFiducialNode(self, newFiducialNode):
self.updateWidgetFromMRML()

def updateWidgetFromMRML(self):
# TODO: There are other widgets and other form fields. What rule determines which need to be included here?
if self.camera:
# View angles are in degrees
self.flythroughViewAngleSlider.value = self.camera.GetViewAngle()
if self.cameraNode:
pass
if self.fiducialNode:
pass
if self.transform:
pass

def onCameraModified(self, observer, eventid):
self.updateWidgetFromMRML()
Expand All @@ -384,10 +378,10 @@ def onFiducialNodeModified(self, observer, eventid):
# TODO: Perhaps invoke EndoscopyLogic only if the Endoscopy module is currently active ... otherwise we are
# potentially computing this for all modifications to all markups, whether or not they will be used with the
# Endoscopy module.
if self.fiducialNode:
if self.logic:
self.logic.cleanup()
self.logic = EndoscopyLogic(self.fiducialNode)
if self.logic:
# We are going to have rebuild the EndoscopyLogic later
self.logic.cleanup()
self.logic = None

def onCreatePathButtonClicked(self):
# TODO: Don't create a model, though do create the cursor (so the user can see where in the flythrough we are)
Expand All @@ -403,6 +397,8 @@ def onCreatePathButtonClicked(self):
fiducialNode = self.inputFiducialNodeSelector.currentNode()
outputPathNode = self.outputPathNodeSelector.currentNode()
self.setFiducialNode(fiducialNode)
if self.logic is None:
self.logic = EndoscopyLogic(self.fiducialNode)
numberOfControlPoints = self.logic.resampledCurve.GetNumberOfControlPoints()
logging.debug(f"-> Computed path contains {numberOfControlPoints} elements")

Expand Down Expand Up @@ -541,13 +537,16 @@ def flyToNext(self):
self.flythroughFrameSlider.value = nextStep

def flyTo(self, resampledCurveControlPointIndex):
# TODO: The flythrough jerks every few frames. Why is that?
if self.logic is None:
# We don't have a curve to fly on. Make one.
self.logic = EndoscopyWidget(self.fiducialNode)

if self.logic.resampledCurve is None:
return
resampledCurveControlPointIndex = int(resampledCurveControlPointIndex)
if not 0 <= resampledCurveControlPointIndex < self.logic.resampledCurve.GetNumberOfControlPoints():
# Probably the curve hasn't been properly built yet, so do nothing.
if (
self.logic.resampledCurve is None
or not 0 <= resampledCurveControlPointIndex < self.logic.resampledCurve.GetNumberOfControlPoints()
):
# There is no curve despite trying. For example, perhaps the user in just staring out making the curve and
# it has no length yet.
return

cameraPosition = np.zeros((3,))
Expand All @@ -571,6 +570,25 @@ def flyTo(self, resampledCurveControlPointIndex):
self.transform.SetMatrixTransformToParent(worldMatrix4x4)
self.cameraNode.EndModify(wasModified)

@staticmethod
def RemoveWidgetsFrom3DView(cameraNode, sequenceOfWidgets):
view_node = slicer.mrmlScene.GetSingletonNode(cameraNode.GetLayoutName(), "vtkMRMLViewNode")
view_id = view_node.GetID()
view_name = view_node.GetName()
logging.debug(f"{view_id = }")
logging.debug(f"{view_name = }")

for widget in sequenceOfWidgets:
logging.debug(f"Processing widget with ID = {widget.GetID()}")
logging.debug(f"Processing widget with name = {widget.GetName()}")
for displayNodeIndex in reversed(range(widget.GetNumberOfDisplayNodes())):
displayNode = widget.GetNthDisplayNode(displayNodeIndex)
if displayNode:
logging.debug(f" Removing display node = {displayNodeIndex}")
displayNode.RemoveViewNodeID(view_id)
else:
logging.debug(f" Not removing display node = {displayNodeIndex}")


class EndoscopyLogic:
"""Compute path given a list of fiducial nodes.
Expand All @@ -595,10 +613,11 @@ class EndoscopyLogic:
)

def __init__(self, inputCurve, dl=0.5):
logging.debug("Calculating Path...")
logging.debug("Creating EndoscopyLogic")
self.cleanup()
self.dl = dl # desired world space step size (in mm)
self.setControlPoints(inputCurve)
logging.debug("Creating EndoscopyLogic ... done")

def __del__(self):
self.cleanup()
Expand All @@ -624,6 +643,8 @@ def setControlPoints(self, inputCurve):
)
return False

if not inputCurve.GetNumberOfDisplayNodes():
inputCurve.CreateDefaultDisplayNodes()
self.inputCurve = inputCurve
self.closed = EndoscopyLogic.curveTypeIsClosed[self.inputCurve.GetClassName()]

Expand All @@ -632,10 +653,17 @@ def setControlPoints(self, inputCurve):

resampledPoints = vtk.vtkPoints()
if self.inputCurve.GetNumberOfControlPoints() > 1:
originalPointsPerSegment = self.inputCurve.GetNumberOfPointsPerInterpolatingSegment()
# We want at least as many points as 8.0 times the number of self.dl intervals that we expect
pointsPerSegment = (
int(self.inputCurve.GetCurveLengthWorld() / self.dl / self.inputCurve.GetNumberOfControlPoints()) + 1
int(
(self.inputCurve.GetCurveLengthWorld() / (self.dl / 8.0))
/ (self.inputCurve.GetNumberOfControlPoints() - 1)
+ 1
)
if self.inputCurve.GetNumberOfControlPoints() > 1
else originalPointsPerSegment
)
originalPointsPerSegment = self.inputCurve.GetNumberOfPointsPerInterpolatingSegment()
if originalPointsPerSegment < pointsPerSegment:
self.inputCurve.SetNumberOfPointsPerInterpolatingSegment(pointsPerSegment)

Expand All @@ -648,33 +676,21 @@ def setControlPoints(self, inputCurve):

self.numberOfResampledCurveControlPoints = resampledPoints.GetNumberOfPoints()

# Make a curve from these resampledPoints
# Make a curve from these resampledPoints. We want all associated information from inputCurve, except its name
# and control points.

self.resampledCurve = slicer.vtkMRMLMarkupsCurveNode()
wasModified = self.resampledCurve.StartModify()
# For resampledCurve, we want all associated information from inputCurve, including any cameraOrientations
self.resampledCurve.Copy(self.inputCurve)
# ... except that we will have a refined set of control points
self.resampledCurve.SetName(f"Resampled-{inputCurve.GetName()}")

self.resampledCurve.RemoveAllControlPoints()
points = np.zeros((self.numberOfResampledCurveControlPoints, 3))
for resampledCurveControlPointIndex in range(self.numberOfResampledCurveControlPoints):
resampledPoints.GetPoint(resampledCurveControlPointIndex, points[resampledCurveControlPointIndex])
self.resampledCurve.AddControlPointWorld(*points[resampledCurveControlPointIndex])
self.resampledCurve.EndModify(wasModified)

# TODO: Delete this debug code
# angles = []
# for pointIndex in range(self.numberOfResampledCurveControlPoints - 2):
# v0 = points[pointIndex + 1, :] - points[pointIndex + 0, :]
# v1 = points[pointIndex + 2, :] - points[pointIndex + 1, :]
# cosine = np.dot(v0, v1) / np.linalg.norm(v0) / np.linalg.norm(v1)
# angles.append(np.arccos(np.clip(cosine, -1.0, 1.0)))
# print(f"{angles = }")
# lengths = [
# np.linalg.norm(points[pointIndex + 1, :] - points[pointIndex + 0, :])
# for pointIndex in range(self.numberOfResampledCurveControlPoints - 1)
# ]
# print(f"{lengths = }")

# ################################################################
# Find a plane that approximately includes the points of the resampled curve, so that we can use its normal to
# define the "up" direction. This is somewhat nonsensical if self.numberOfResampledCurveControlPoints < 3, but
Expand Down Expand Up @@ -793,7 +809,8 @@ def worldOrientationToRelative(
@staticmethod
def DistanceAlongCurveOfNthControlPoint(curve, indexOfControlPoint):
controlPointPositionWorld = curve.GetNthControlPointPositionWorld(indexOfControlPoint)
# Curve points are about 10-to-1 control points. There are index + 1 points in {0, ..., index}. So, typically
# There are self.curve.GetNumberOfPointsPerInterpolatingSegment() -- usually 10 -- of the "curve point" for each
# "control point". There are index + 1 curve points in {0, ..., index}. So, typically, we have
# numberOfCurvePoints = 10 * indexOfControlPoint + 1.
numberOfCurvePoints = curve.GetClosestCurvePointIndexToPositionWorld(controlPointPositionWorld) + 1
distance = curve.GetCurveLengthWorld(0, numberOfCurvePoints)
Expand Down Expand Up @@ -948,7 +965,8 @@ def __init__(self, resampledCurve, inputCurve, cameraNode, outputPathNode=None,
"vtkMRMLModelNode", slicer.mrmlScene.GenerateUniqueName(f"Path-{inputCurve.GetName()}")
)
model.CreateDefaultDisplayNodes()
model.GetDisplayNode().SetColor(1, 1, 0) # yellow
for displayNodeIndex in range(model.GetNumberOfDisplayNodes()):
model.GetNthDisplayNode(displayNodeIndex).SetColor(1, 1, 0) # yellow

model.SetAndObservePolyData(polyData)

Expand All @@ -961,9 +979,11 @@ def __init__(self, resampledCurve, inputCurve, cameraNode, outputPathNode=None,
"vtkMRMLMarkupsFiducialNode", slicer.mrmlScene.GenerateUniqueName(f"Cursor-{inputCurve.GetName()}")
)
cursor.CreateDefaultDisplayNodes()
cursor.GetDisplayNode().SetSelectedColor(1, 0, 0) # red
cursor.GetDisplayNode().SetSliceProjection(True)
cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside
for displayNodeIndex in range(cursor.GetNumberOfDisplayNodes()):
displayNode = cursor.GetNthDisplayNode(displayNodeIndex)
displayNode.SetSelectedColor(1, 0, 0) # red
displayNode.SetSliceProjection(True)
displayNode.BackfaceCullingOn() # so that the camera can see through the cursor from inside
cursor.AddControlPoint(vtk.vtkVector3d(0, 0, 0), " ") # do not show any visible label
cursor.SetNthControlPointLocked(0, True)
else:
Expand All @@ -972,20 +992,21 @@ def __init__(self, resampledCurve, inputCurve, cameraNode, outputPathNode=None,
"vtkMRMLMarkupsModelNode", slicer.mrmlScene.GenerateUniqueName(f"Cursor-{inputCurve.GetName()}")
)
cursor.CreateDefaultDisplayNodes()
cursor.GetDisplayNode().SetColor(1, 0, 0) # red
cursor.GetDisplayNode().BackfaceCullingOn() # so that the camera can see through the cursor from inside
for displayNodeIndex in range(cursor.GetNumberOfDisplayNodes()):
displayNode = cursor.GetNthDisplayNode(displayNodeIndex)
displayNode.SetColor(1, 0, 0) # red
displayNode.BackfaceCullingOn() # so that the camera can see through the cursor from inside
# Add a sphere as cursor
sphere = vtk.vtkSphereSource()
sphere.Update()
cursor.SetPolyDataConnection(sphere.GetOutputPort())

model.SetNodeReferenceID("CameraCursor", cursor.GetID())

# TODO: Why is the following not working? Do we need to hide inputCurve and resampledCurve too?
view_node = slicer.mrmlScene.GetSingletonNode(cameraNode.GetLayoutName(), "vtkMRMLViewNode")
view_id = view_node.GetID()
cursor.GetDisplayNode().RemoveViewNodeID(view_id)
model.GetDisplayNode().RemoveViewNodeID(view_id)
# Hide our flythough cursor and path from the 3-d viewing pane
# TODO: Why doesn't this hide the inputCurve? When the path is wholly in the R slice, why do we still see the
# cursor, and why does this inputCurve completely dominate the view only in this case?
EndoscopyWidget.RemoveWidgetsFrom3DView(cameraNode, [cursor, model, inputCurve])

# Transform node
transform = model.GetNodeReference("CameraTransform")
Expand Down

0 comments on commit 39cfc8a

Please sign in to comment.