Create a plane that is perpendicular to another plane

I’ve created a best fit plane from a point list in the Markups module and I would like to create a second plane perpendicular to the best fit plane. Attached is an example using one of the mouse crania from the SlicerMorph sample data.

Here, I created a best fit plane (Plane A) using landmarks 13,14,15,28,54, and 55. I would like to create a second perpendicular plane (Plane B) so that Plane A represents the X-axis and Plane B represents a Y-Axis.

I looked over the documentation and scripts repository but I’m also not well versed in python so I’m not sure what to look for. Any help would be appreciated.

Could you not also set Plane B using the Markups module? You only need two landmarks. I think you can set the plane normal constrained relative to Plane A or the plane origin at a landmark/centroid?

It is very easy, have a look at this tutorial to see how to create orthogonal planning.https://youtu.be/Bs4GWLrFQfA

Yes, the idea would be to add a second plane in the Markups module, but where I’m having issues is figuring out how to constrain it to be perpendicular relative to plane A.

If you don’t want to use python scripting, you may consider the Reslice functions in this project.

  • Reslice a view be perpendicular to that plane (choose Binormal or Tangent).
  • Place a second plane markups node in the latter slice view.
  • Restore the orientation of the slice view.

@chir.set Thanks for sharing. I installed the tool but I’m only working with surface PLY files, so I dont have slices.

Also, I’m not opposed to using python, I just don’t know how to use python but I’m willing to learn.

Thank you for sharing. That is quite easy however I’m only working with surface PLY files, not CT/DICOM files.

You don’t need a volume for that. The slice views can be empty. You don’t even need a model.

@chir.set Got it, thanks for the clarification. I was using a 3D only view and was confused why nothing seemed to be happening. Just tried it again and it works remarkably well!

I did notice that because the my surface is not aligned along the axis, I had to rotate the new plane created (Plane B; Green) along the one axis to get it to run along the direction I’m interested in (Plane B - Rotated; Blue). Is there a way to accomplish this without having to manually rotate it?

You can proceed this way:

  • Check ‘Orthogonal intersection’ when you orient a slice view to be parallel to your first plane node (use Normal).
  • Activate ‘Slice intersections’ in the slice view (right click).
  • Rotate the other slice views in it, by ‘Ctrl+Alt+Mouse move’ or by checking ‘Interaction’ to activate the handles; rotate so that one of the views is as you want (see the line colour in the slice view).
  • In the target slice view (line colour in the source slice view), place a new plane markups node.
  • Optionally restore the orientation slice views.

This worked perfectly, thank you!

I have a helper script that might be helpful.

################# Create points at 4, 7, 10mm
import numpy as np

Given points

P0 = np.array([-95.807, 86.787, -0.063])
P1 = np.array([-96.027, 87.707, -4.015])

Direction vector

v = P1 - P0

Normalize

v_hat = v / np.linalg.norm(v)

Distances you want (mm)

distances = [4, 7, 10]

print("Unit direction vector:", v_hat, "\n")

for d in distances:
P = P0 + d * v_hat
print(f"Point at {d} mm:", P)

#####Create horizontal planes at 4, 7, 10mm
def createPlanes(f, plane2Center=None, angle=None, useBinormal=True,
plane1Name="Plane1", plane2Name="Plane_7mm"):
"""
f: vtkMRMLMarkupsFiducialNode (or any markups node with >=3 control points)
plane2Center: [x,y,z] world coords where plane2 passes through (default: p1)
angle: rotation (degrees) to spin perpendicular choice around plane1 normal (default: 0)
useBinormal: True -> use binormal as plane2 normal, False -> use tangent
"""

# --- Read 3 points from markups node f ---
p1 = [0.0, 0.0, 0.0]
p2 = [0.0, 0.0, 0.0]
p3 = [0.0, 0.0, 0.0]
f.GetNthControlPointPositionWorld(0, p1)
f.GetNthControlPointPositionWorld(1, p2)
f.GetNthControlPointPositionWorld(2, p3)

rotationOfPerpendiculars = float(angle) if angle is not None else 0.0

plane2Coordinates = plane2Center if plane2Center is not None else p1

# --- Create Plane1 (3-point plane) ---
plane1 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsPlaneNode", plane1Name)
plane1.CreateDefaultDisplayNodes()
plane1.SetPlaneType(slicer.vtkMRMLMarkupsPlaneNode.PlaneType3Points)
plane1.AddControlPointWorld(p1)
plane1.AddControlPointWorld(p2)
plane1.AddControlPointWorld(p3)

# --- Get Plane1 normal (world) ---
plane1Normal = [0.0, 0.0, 0.0]
plane1.GetNormalWorld(plane1Normal)
vtk.vtkMath.Normalize(plane1Normal)

# --- Compute perpendicular directions to plane1Normal ---
plane1Binormal = [0.0, 0.0, 0.0]
plane1Tangent  = [0.0, 0.0, 0.0]
vtk.vtkMath.Perpendiculars(plane1Normal, plane1Binormal, plane1Tangent, rotationOfPerpendiculars)

n2 = plane1Binormal if useBinormal else plane1Tangent
vtk.vtkMath.Normalize(n2)

# --- Create Plane2 (point + normal) ---
plane2 = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLMarkupsPlaneNode", plane2Name)
plane2.CreateDefaultDisplayNodes()
plane2.AddControlPointWorld(plane2Coordinates)
plane2.SetNormalWorld(n2)

print("Plane1 points:", p1, p2, p3)
print("Plane1 normal:", plane1Normal)
print("Plane2 origin:", plane2Coordinates)
print("Plane2 normal:", n2)

return plane1, plane2

f = slicer.util.getNode("F_1")  # replace "F" with your fiducials node name
plane1, plane2 = createPlanes(
f,
plane2Center=[-96.348, 89.051, -9.788],
angle=0.0,
useBinormal=False
)
#useBinormal = True create another perpendicular plane

This is for another work so the naming and some parts were a bit confusing. You could pretty much ignore lines 1-23.

You need to first create a fiducial markup F_1 or your preferred name (you need to change line 82 getNode("F_1")) with three points to define a plane. You can place the three points along your mid-sagittal plane. It will create a plane called Plane

Then you need to define plane2Center=[-96.348, 89.051, -9.788]at line 85. You can probably switch the numbers to the coordinates of your sagittal plane’s center.

It will create a perpendicular plane named as Plane_7mm according to the normal of plane1.GetNormalWorld(plane1Normal).

Hopefully it works for your case.