How to computer normals from a Model

Hi, everybody.

I am a software engineer who also designs tracheostomy tubes for my medical advocacy clients (Paraclete Advocacy, Ontario, Canada) and have found Slicer extremely helpful, both to create new tube designs (with the help of SolidWorks and Blender) and also to do virtual insertions of trach tube models into DICOM data from CAT scans and to measure scanned tubes.

I’ve been trying to “up my game” by going less by “look and feel” and becoming more precise and repeatable by automating portions of my workflow. One such example is producing normal vectors at the proximal and distal centerlines of candidate trach tube designs.

Surprise surprise, ChatGPT got it right this time. This snippet of code allowed me to take a point set csv from a trach tube manufacturer representing the centerline of a manufactured tube, and via the CurveMaker extension, allow me to wrap the centerline with a curvy cylinder and then save it as a model.

The following script allowed me to create a complete set of normals for every point in the model (I only really needed normals form the top/bottom faces but this worked as well). The normals came in as a separate object that I could display/hide at will from my data view. I was able to quickly measure the proposed tube shape and compare it against the design that I had created that ultimately caused the manufacturer to generate the shape.

For the first time ever using the Python Interactor, I have to say it was a wonderful experience with a little help from ChatGPT. I have industry experience with Python development but since this work is not billable, I hadn’t delved too far into slicer Python handshaking. This was a really nice working introduction that I wanted to share, since it helped me serve a client with more accuracy than in the past.

Medical GPT
To visualize surface normals as lines on a model in 3D Slicer, you can use Python scripting within Slicer to programmatically create line representations of the normals and then add these to the 3D display. Here is a step-by-step guide on how to accomplish this:

Step 1: Prepare Your Model
Ensure your model is loaded in 3D Slicer. You’ll need to work with this model’s mesh data.

Step 2: Open Python Interactor
Open the Python Interactor in 3D Slicer, which can be accessed from the menu or by pressing Ctrl+3.

Step 3: Script to Create Lines for Normals
The following Python script will:

Compute the normals for the mesh.
Create lines for each normal.
Display these lines in the 3D view.
python
Copy code

# Replace 'ModelName' with the name of your model in the scene
modelNode = slicer.util.getNode('ModelName')
polyData = modelNode.GetPolyData()

# Ensure normals are computed
polyData.BuildNormals()

# Access the normals
normals = polyData.GetPointData().GetNormals()

# Create a new model for the normals display
linesNode = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelNode", "NormalsVisualization")
linePolyData = vtk.vtkPolyData()
linesNode.SetAndObservePolyData(linePolyData)

# Prepare to create lines
points = vtk.vtkPoints()
lines = vtk.vtkCellArray()
linePolyData.SetPoints(points)
linePolyData.SetLines(lines)

# Scaling factor for normal vectors visualization
scaleFactor = 10.0

# Create lines representing normals
for i in range(normals.GetNumberOfTuples()):
    x, y, z = polyData.GetPoint(i)
    nx, ny, nz = normals.GetTuple(i)
    
    # Start point of the line
    start = [x, y, z]
    # End point of the line (extending the normal by scaleFactor)
    end = [x + nx * scaleFactor, y + ny * scaleFactor, z + nz * scaleFactor]
    
    idxStart = points.InsertNextPoint(start)
    idxEnd = points.InsertNextPoint(end)
    
    line = vtk.vtkLine()
    line.GetPointIds().SetId(0, idxStart)
    line.GetPointIds().SetId(1, idxEnd)
    lines.InsertNextCell(line)

# Update model display properties
modelDisplay = slicer.mrmlScene.AddNewNodeByClass("vtkMRMLModelDisplayNode")
modelDisplay.SetColor(1, 0, 0)  # Red color
modelDisplay.SetLineWidth(2)    # Line width
linesNode.SetAndObserveDisplayNodeID(modelDisplay.GetID())

# Refresh to show lines
linesNode.CreateDefaultDisplayNodes()
linePolyData.Modified()

Step 4: Adjust Parameters
Scaling Factor: Adjust scaleFactor to make the normal lines longer or shorter as needed to better visualize them against the model.
Color and Line Width: Set the color and line width to make the normal lines clearly visible.
Step 5: Run the Script
Paste and run this script in the Python Interactor. The normals will be visualized as red lines extending from the surface of your model in the 3D view.

Conclusion
This script creates a separate model node to hold the visualization of the normals, allowing you to easily toggle their visibility or remove them without affecting the original model. You can customize this further as needed for specific visualization requirements or to include in a custom Slicer module for more interactive use.

ChatGPT can make mistakes. Check important info.
?
ChatGPT

1 Like

This all sounds great, thanks for sharing. It would awesome if you could also post some screenshots or photos of your results.

By request, I’m uploading some visuals.

Here are two visuals with normals (in red color) visible (end and side views)

1 Like

Here are another two visuals with my measurements (with alignment of markups confirmed not only by eye but also by normals, top and side views).

3 Likes