Hi. I am an interventional cardiologist. Currently I am interested in pre-planning for my intervention. In detail, I want to determine the optimal angles for setting the X-ray views.
Is there any extension, or Python codes for displaying these parameter (as I show in the following image).
There isn’t a dedicated planning tool that I’m aware of. You may want to just use Markups Angles to design your imaging plan and then match the device as best you can.
You can copy-paste this code snippet to the Python console to show the anatomical angles (LAO/RAO, CRA/CAU) of the C-arm in the corner of a 3D view:
threeDViewIndex = 0 # change this to show angles in a different 3D view
def positionerAngleFromViewNormal(viewNormal):
# According to https://www5.informatik.uni-erlangen.de/Forschung/Publikationen/2014/Koch14-OVA.pdf
nx = -viewNormal[0] # L
ny = -viewNormal[1] # P
nz = viewNormal[2] # S
import math
if abs(ny) > 1e-6:
primaryAngleDeg = math.atan(-nx/ny) * 180.0 / math.pi
elif nx >= 0:
primaryAngleDeg = 90.0
else:
primaryAngleDeg = -90.0
secondaryAngleDeg = math.asin(nz) * 180.0 / math.pi
return [primaryAngleDeg, secondaryAngleDeg]
def formatPositionerAngle(positionerAngles):
primaryAngleDeg, secondaryAngleDeg = positionerAngles
text = f'{"RAO" if primaryAngleDeg < 0 else "LAO"} {abs(primaryAngleDeg):.1f}\n'
text += f'{"CRA" if secondaryAngleDeg < 0 else "CAU"} {abs(secondaryAngleDeg):.1f}'
return text
def cameraUpdated(cameraNode, view):
viewNormal = cameraNode.GetCamera().GetDirectionOfProjection()
positionerAngleText = formatPositionerAngle(positionerAngleFromViewNormal(viewNormal))
view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperRight, positionerAngleText)
view.cornerAnnotation().GetTextProperty().SetColor(1,1,0) # yellow
view.scheduleRender()
layoutManager = slicer.app.layoutManager()
view = layoutManager.threeDWidget(threeDViewIndex).threeDView()
threeDViewNode = view.mrmlViewNode()
cameraNode = slicer.modules.cameras.logic().GetViewActiveCameraNode(threeDViewNode)
cameraObservation = cameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, lambda caller, event, view=view: cameraUpdated(caller, view))
cameraUpdated(cameraNode, view)
# Execute the next line to stop updating the positioner angles in the view corner
# cameraNode.RemoveObserver(cameraObservation)
We are developing a fluoro simulator module for interventional cardiology as part of the SlicerHeart project. The module will be able to display not just the angles but simulated fluoro images and compute optimal viewing angles, etc. We’ll release it when the results are first published - probably within a year.
Hi, my image is XA, I want to display the anatomical angles of the C-arm (LAO/RAO, CRA/CAU) in the corner of the red slice viewer, I try to modify your code, but an error is reported when running, how do I modify it?
def positionerAngleFromViewNormal(viewNormal):
# According to https://www5.informatik.uni-erlangen.de/Forschung/Publikationen/2014/Koch14-OVA.pdf
nx = -viewNormal[0] # L
ny = -viewNormal[1] # P
nz = viewNormal[2] # S
import math
if abs(ny) > 1e-6:
primaryAngleDeg = math.atan(-nx/ny) * 180.0 / math.pi
elif nx >= 0:
primaryAngleDeg = 90.0
else:
primaryAngleDeg = -90.0
secondaryAngleDeg = math.asin(nz) * 180.0 / math.pi
return [primaryAngleDeg, secondaryAngleDeg]
def formatPositionerAngle(positionerAngles):
primaryAngleDeg, secondaryAngleDeg = positionerAngles
text = f'{"RAO" if primaryAngleDeg < 0 else "LAO"} {abs(primaryAngleDeg):.1f}\n'
text += f'{"CRA" if secondaryAngleDeg < 0 else "CAU"} {abs(secondaryAngleDeg):.1f}'
return text
def cameraUpdated(cameraNode, view):
viewNormal = cameraNode.GetCamera().GetDirectionOfProjection()
positionerAngleText = formatPositionerAngle(positionerAngleFromViewNormal(viewNormal))
view.cornerAnnotation().SetText(vtk.vtkCornerAnnotation.UpperRight, positionerAngleText)
view.cornerAnnotation().GetTextProperty().SetColor(1,1,0) # yellow
view.scheduleRender()
layoutManager = slicer.app.layoutManager()
sliceViewName = layoutManager.sliceViewNames()[0]
view = layoutManager.sliceWidget(sliceViewName).sliceView()
sliceViewNode = view.mrmlViewNode()
cameraNode = slicer.modules.cameras.logic().GetViewActiveCameraNode(sliceViewNode)
cameraObservation = cameraNode.AddObserver(vtk.vtkCommand.ModifiedEvent, lambda caller, event, view=view: cameraUpdated(caller, view))
cameraUpdated(cameraNode, view)
>>> sliceViewNode = view.mrmlViewNode()
Traceback (most recent call last):
File "<console>", line 1, in <module>
AttributeError: qMRMLSliceView has no attribute named 'mrmlViewNode'
I need to display angles in red slice viewer, the image is 2D, so I modified your 3D viewer code, but it is wrong, I don’t know how to modify it.
The script above solves a more complex task: computing C-arm angles from the camera viewpoint in a 3D view. You can use it to find a good angulation that you can just on your C-arm.
If you already have an X-ray angiography image sequence then the C-arm angles are already stored in the DICOM series for each frame, we just need to display it.
For a dynamic acquisition - for showing angles changing throughout the series - we would need to create a somewhat complex script. For showing angles for a static-gantry image we could easily just change the DICOM importer to include th C-arm angle in the node name.
What is your overall goal? How do you plan to use XA images in Slicer?
I just want to display the C-arm angles in red slice viewer. I found the angles are in the DICOM tags:(0018,1510) DS PositionerPrimaryAngle, (0018,1511) DS PositionerSecondaryAngle. How can I make it display as (LAO/RAO, CRA/CAU) in the red slice viewer?
The acquisition process does not move, and each sequence has an angle. I want to display the angle of the sequence after opening a sequence. The angle information is recorded in the Dicom tag, but I don’t know how to display it.
Yes, it’s enough, I just want the angle to be displayed. I don’t know how to get the value of PositionerPrimaryAngle and PositionerSecondaryAngle, and then display it on the red slice viewer. I only know that I can get the angle through the following function by getting those two values:
def formatPositionerAngle(primaryAngleDeg, secondaryAngleDeg):
text = f'{"RAO" if primaryAngleDeg < 0 else "LAO"} {abs(primaryAngleDeg):.1f}\n'
text += f'{"CRA" if secondaryAngleDeg < 0 else "CAU"} {abs(secondaryAngleDeg):.1f}'
return text
The angles could be added to the name of thr image at the time of DICOM import. The importer is this Python script, you can modify it on your computer.
I was wondering if the fluoro simulator module in the SlicerHeart project has been released yet. I could really use it for my procedure planning! I was also wondering how accurate the simulated fluoro images will be; for instance, will they take into account the fanning of the x-ray beam and the distance from the x-ray tube focal spot to the anatomy?
Thank you very much for the quick reply. I look forward to trying the fluoro simulator, but if you haven’t submitted the article yet, then there is no telling how long it may take… I can’t wait that long, so I am working on a poor-man’s version on my own.
I found your code for extracting or defining the c-arm angles, and I figured out how to adjust the colors and opacity to get a reasonable fluoro simulated images, but I am stuck on one issue… the standard fluoro image display shows the projection as viewed from the detector (head up, left side of body on right side of image when at orientation 0,0). In slicer, the image is as viewed from the camera which mimics the x-ray tube in terms of the perspective. For this reason the images that I obtain are flipped L-R relative to the standard fluoro display.
If you want to flip between showing the front or back of the mesh, you can use reverse perspective projection in the renderer camera. We have discussed this a couple of times on the VTK forum (for example, here).
In the simulator in SlicerHeart we do not support flipping between rendering from the front or back, as we could achieve all visualizations that we needed without implementing this (by rendering 3D content semitransparent). Also, there may be a few things that need to be fixed (one issue that we know about is that currently GPU volume rendering is not compatible with reverse perspective projection).