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 3113739 commit cc99284
Showing 1 changed file with 58 additions and 32 deletions.
90 changes: 58 additions & 32 deletions Modules/Scripted/Endoscopy/Endoscopy.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,8 @@ def setupPathUI(self):
inputFiducialNodeSelector = slicer.qMRMLNodeComboBox()
logging.debug(" inputFiducialNodeSelector = slicer.qMRMLNodeComboBox()")
inputFiducialNodeSelector.objectName = "inputFiducialNodeSelector"
inputFiducialNodeSelector.toolTip = _("Select a fiducial list to define control points for the path.")
inputFiducialNodeSelector.nodeTypes = ["vtkMRMLMarkupsFiducialNode", "vtkMRMLMarkupsCurveNode"]
inputFiducialNodeSelector.toolTip = _("Select a curve to define control points for the path.")
inputFiducialNodeSelector.nodeTypes = list(EndoscopyLogic.curveTypeIsClosed.keys())
inputFiducialNodeSelector.noneEnabled = False
inputFiducialNodeSelector.addEnabled = False
inputFiducialNodeSelector.removeEnabled = False
Expand All @@ -140,7 +140,7 @@ def setupPathUI(self):
outputPathNodeSelector = slicer.qMRMLNodeComboBox()
logging.debug(" outputPathNodeSelector = slicer.qMRMLNodeComboBox()")
outputPathNodeSelector.objectName = "outputPathNodeSelector"
outputPathNodeSelector.toolTip = _("Select a fiducial list to define control points for the path.")
outputPathNodeSelector.toolTip = _("Create a model node.")
outputPathNodeSelector.nodeTypes = ["vtkMRMLModelNode"]
outputPathNodeSelector.noneEnabled = False
outputPathNodeSelector.addEnabled = True
Expand All @@ -155,9 +155,9 @@ def setupPathUI(self):
self.outputPathNodeSelector = outputPathNodeSelector

# CreatePath button
createPathButton = qt.QPushButton(_("Create model"))
logging.debug(' createPathButton = qt.QPushButton("Create model")')
createPathButton.toolTip = _("Create the path.")
createPathButton = qt.QPushButton(_("Create flythrough and model"))
logging.debug(' createPathButton = qt.QPushButton(_("Create flythrough and model"))')
createPathButton.toolTip = _("Create the flythough and a model.")
createPathButton.enabled = False
createPathButton.connect("clicked()", self.onCreatePathButtonClicked)
logging.debug(' createPathButton.connect("clicked()", self.onCreatePathButtonClicked)')
Expand Down Expand Up @@ -381,11 +381,12 @@ def enableOrDisableCreateButton(self):

def onFiducialNodeModified(self, observer, eventid):
"""If the fiducial was changed we need to repopulate the keyframe UI"""
# Hack rebuild path just to get the new data
# 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()
# TODO: If this proves to be too slow, look for a faster way than total reconstruction
self.logic = EndoscopyLogic(self.fiducialNode)

def onCreatePathButtonClicked(self):
Expand All @@ -401,16 +402,12 @@ def onCreatePathButtonClicked(self):

fiducialNode = self.inputFiducialNodeSelector.currentNode()
outputPathNode = self.outputPathNodeSelector.currentNode()
logging.debug("Calculating Path...")
if self.logic:
self.logic.cleanup()
self.logic = EndoscopyLogic(fiducialNode)
self.setFiducialNode(fiducialNode)
numberOfControlPoints = self.logic.resampledCurve.GetNumberOfControlPoints()
logging.debug(f"-> Computed path contains {numberOfControlPoints} elements")

# TODO: Build cursor and its transform, but not this model
logging.debug("Create model...")
model = EndoscopyPathModel(self.logic.resampledCurve, fiducialNode, outputPathNode)
model = EndoscopyPathModel(self.logic.resampledCurve, fiducialNode, self.cameraNode, outputPathNode)
logging.debug("-> Model created")

# Update frame slider range
Expand Down Expand Up @@ -549,6 +546,9 @@ def flyTo(self, resampledCurveControlPointIndex):
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.
return

cameraPosition = np.zeros((3,))
self.logic.resampledCurve.GetNthControlPointPositionWorld(resampledCurveControlPointIndex, cameraPosition)
Expand Down Expand Up @@ -589,11 +589,13 @@ class EndoscopyLogic:
"""

# For each supported curve type, indicate whether it is a closed-curve type.
# TODO: vtkMRMLMarkupsFiducialNode doesn't support GetCurveLengthWorld()?
curveTypeIsClosed = dict(
vtkMRMLMarkupsClosedCurveNode=True, vtkMRMLMarkupsCurveNode=False, vtkMRMLMarkupsFiducialNode=False
)

def __init__(self, inputCurve, dl=0.5):
logging.debug("Calculating Path...")
self.cleanup()
self.dl = dl # desired world space step size (in mm)
self.setControlPoints(inputCurve)
Expand All @@ -606,7 +608,6 @@ def cleanup(self):
self.closed = None
self.dl = None
self.inputCurve = None
self.numberOfInputPoints = None
self.numberOfResampledCurveControlPoints = None
self.resampledCurve = None
self.planePosition = None
Expand All @@ -624,20 +625,28 @@ def setControlPoints(self, inputCurve):
return False

self.inputCurve = inputCurve
self.numberOfInputPoints = self.inputCurve.GetNumberOfControlPoints()
self.closed = EndoscopyLogic.curveTypeIsClosed[self.inputCurve.GetClassName()]

# If self.numberOfInputPoints < 2 then there isn't much to do, but we'll fall through anyway

# ################################################################
# Internally, we'll want to precompute steps along the curve of length self.dl and camera orientations for them.

resampledPoints = vtk.vtkPoints()
slicer.vtkMRMLMarkupsCurveNode.ResamplePoints(
self.inputCurve.GetCurvePointsWorld(), resampledPoints, self.dl, self.closed
)
self.numberOfResampledCurveControlPoints = resampledPoints.GetNumberOfPoints()
if self.inputCurve.GetNumberOfControlPoints() > 1:
pointsPerSegment = (
int(self.inputCurve.GetCurveLengthWorld() / self.dl / self.inputCurve.GetNumberOfControlPoints()) + 1
)
originalPointsPerSegment = self.inputCurve.GetNumberOfPointsPerInterpolatingSegment()
if originalPointsPerSegment < pointsPerSegment:
self.inputCurve.SetNumberOfPointsPerInterpolatingSegment(pointsPerSegment)

slicer.vtkMRMLMarkupsCurveNode.ResamplePoints(
self.inputCurve.GetCurvePointsWorld(), resampledPoints, self.dl, self.closed
)

if originalPointsPerSegment < pointsPerSegment:
self.inputCurve.SetNumberOfPointsPerInterpolatingSegment(originalPointsPerSegment)

# If self.numberOfResampledCurveControlPoints < 2 then there isn't much to do, but we'll fall through anyway
self.numberOfResampledCurveControlPoints = resampledPoints.GetNumberOfPoints()

# Make a curve from these resampledPoints
self.resampledCurve = slicer.vtkMRMLMarkupsCurveNode()
Expand All @@ -652,9 +661,24 @@ def setControlPoints(self, inputCurve):
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 requires self.numberOfResampledCurveControlPoints >= 3.
# define the "up" direction. This is somewhat nonsensical if self.numberOfResampledCurveControlPoints < 3, but
# proceed anyway.
self.planePosition, self.planeNormal = EndoscopyLogic.PlaneFit(points.T)

# Interpolate the user-supplied orientations to compute (and assign) an orientation to every control point of
Expand All @@ -678,6 +702,7 @@ def interpolateOrientations(self):
self.quaternionInterpolator.SetInterpolationTypeToLinear()

inputCurveLength = self.inputCurve.GetCurveLengthWorld()
resampledCurveLength = self.resampledCurve.GetCurveLengthWorld()
distances = sorted({0.0} | set(cameraOrientations.keys()) | {inputCurveLength})

for distanceAlongInputCurve in distances:
Expand All @@ -697,7 +722,7 @@ def interpolateOrientations(self):

# ################################################################
# Use the configured vtkQuaternionInterpolator to pre-compute orientations for the resampledCurve.
fudgeFactor = inputCurveLength / self.resampledCurve.GetCurveLengthWorld()
fudgeFactor = inputCurveLength / resampledCurveLength if resampledCurveLength else 1.0
wasModified = self.resampledCurve.StartModify()
# The curves have different resolutions so their lengths won't come out exactly the same. We scale the
# distances along self.resampledCurve with a fudgeFactor so that the lengths do come out the same.
Expand Down Expand Up @@ -835,7 +860,7 @@ def PlaneFit(points):
"""

points = points.reshape((points.shape[0], -1)) # Collapse trailing dimensions
p = points.mean(axis=1)
p = points.mean(axis=1) if points.size else np.zeros((points.shape[0],))
points -= p[:, np.newaxis] # Recenter on the centroid
n = np.linalg.svd(np.dot(points, points.T))[0][:, -1]
# Choose the normal to be in the direction of increasing coordinate value
Expand Down Expand Up @@ -877,7 +902,7 @@ class EndoscopyPathModel:
- Add a single polyline
"""

def __init__(self, resampledCurve, inputCurve, outputPathNode=None, cursorType=None):
def __init__(self, resampledCurve, inputCurve, cameraNode, outputPathNode=None, cursorType=None):
"""
:param resampledCurve: resampledCurve generated by EndoscopyLogic
:param inputCurve: input node, just used for naming the output node.
Expand Down Expand Up @@ -939,11 +964,6 @@ def __init__(self, resampledCurve, inputCurve, outputPathNode=None, cursorType=N
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

# TODO: something like this:
# viewnode = slicer.mrmlScene.GetSingletonNode(self.cameraNode.GetLayoutName(), "vtkMRMLViewNode")
# viewid = viewnode.GetID()
# cursor.GetDisplayNode().RemoveViewNodeID(viewid)
cursor.AddControlPoint(vtk.vtkVector3d(0, 0, 0), " ") # do not show any visible label
cursor.SetNthControlPointLocked(0, True)
else:
Expand All @@ -961,6 +981,12 @@ def __init__(self, resampledCurve, inputCurve, outputPathNode=None, cursorType=N

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)

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

0 comments on commit cc99284

Please sign in to comment.