Hi,
I have segmented the liver from a 3D-CT volume and created a volumetric
mesh. I have successfully generated DRRs by using RTK tool kit. The code is
attached below. Now what I want is that I need to project each and every 3D
coordinates of the volumetric mesh on this DRR image plane. This means that
finding the corresponding 2D projection coordinate (i.e. x, y positions)
of each vertex coordinate on the DRR image projection plane.
How can I obtain the (x, y) coordinate values in the DRR image plane for a
given 3D coordinate position? For example, I have (x,y,z) positions for
each vertex and I want to convert into (x,y) i.e. 2D projection coordinates
on the DRR image plane? Can you please elaborate with an example ?
I have python code to project the 3D-mesh coordinates on DRR images based
on ITK SiddonJacobsRayTracing algorithm. However, I want to implement this
code similar to the RTK based DRR projections. I have attached this ITK
based DRR generation code and the point projection code which is compatible
to this ITK-based DRR code for your further reference. Moreover, I have
attached sample DRR image with projected points overlaid on it that is
based on ITK-based code.
*Additional note:* The reason because I have chosen RTK since it is more
convenient than ITK based ray-tracing algorithm when generating DRRs so
that I’m able to generate DRRs to match the exact field of view of real kV
planar x-ray images.
Thanks,
Surang
import os
import itk
from itk import RTK as rtk
import numpy as np
import pydicom as dicom
def generateDRRsRTK(input_file=None, output_file=None, sid=1000., sdd=1536., gantryAngle=0.,
projOffsetX=0., projOffsetY=0., outOfPlaneAngle=0., inPlaneAngle=0.,
sourceOffsetX=0., sourceOffsetY=0., dx=512, dy=512):
CT = itk.imread(input_file, pixel_type=itk.F)
# The CT volume is not correct orientation compatible to the RTK convention and point (0,0,0) mm is not in the CT image
# and this is the default centre of rotation in RTK. Therefore change the origin and the direction to use
# RTK convention to get the correct DRR as expected.
# the input of the Joseph-Filter needs to be oriented in the y-direction. In RTK, the rotation axis is y.
# changed the direction and the iamge origin of the image volume to have the volume in the xz-layer in the y-direction.
# Three rotation angles are used to define the orientation of the detector.
# The ZXY convention of Euler angles is used for detector orientation where GantryAngle is the rotation around y,
# OutOfPlaneAngle the rotation around x and InPlaneAngle the rotation around z.
# change the direction and origin to align with the RTK convention
CTDirection=np.zeros([3,3])
CTDirection[0,0]=1.
CTDirection[1,2]=1.
CTDirection[2,1]=1.
CT.SetDirection(itk.matrix_from_array(CTDirection))
CT.SetOrigin([CT.GetOrigin()[0],CT.GetOrigin()[2],-CT.GetOrigin()[1]])
CTarray = itk.array_view_from_image(CT)
# add 1000 to CT numbers to put air at 0
CTarray += 1000
# Defines the image type
Dimension_CT = 3
PixelType = itk.F
ImageType = itk.Image[PixelType, Dimension_CT]
# Create a stack of empty projection images
ConstantImageSourceType = rtk.ConstantImageSource[ImageType]
constantImageSource = ConstantImageSourceType.New()
# Set origin and spacing based on the Elekta configuration
constantImageSource.SetOrigin([-204.4,-204.4,0])
constantImageSource.SetSpacing([0.8,0.8,0.8])
constantImageSource.SetSize([dx, dy, 1])
constantImageSource.SetConstant(0.)
# Defines the RTK geometry object
geometry = rtk.ThreeDCircularProjectionGeometry.New()
geometry.AddProjection(sid, sdd, gantryAngle, projOffsetX, projOffsetY, outOfPlaneAngle, inPlaneAngle, sourceOffsetX, sourceOffsetY)
REIType = rtk.JosephForwardProjectionImageFilter[ImageType, ImageType]
rei = REIType.New()
rei.SetGeometry(geometry)
rei.SetInput(0, constantImageSource.GetOutput())
rei.SetInput(1, CT)
rei.Update()
Dimension = 3
OutputPixelType = itk.UC
OutputImageType = itk.Image[OutputPixelType, Dimension]
RescaleType = itk.RescaleIntensityImageFilter[ImageType, OutputImageType]
rescaler = RescaleType.New()
rescaler.SetOutputMinimum(0)
rescaler.SetOutputMaximum(255)
rescaler.SetInput(rei.GetOutput())
rescaler.Update()
# Out of some reason, the computed projection is upsided-down.
# Here we use a FilpImageFilter to flip the images in y direction.
FlipFilterType = itk.FlipImageFilter[OutputImageType]
flipFilter = FlipFilterType.New()
FlipAxesArray = itk.FixedArray[itk.D, 3]()
FlipAxesArray[0] = 0
FlipAxesArray[1] = 1
FlipAxesArray[2] = 0
flipFilter.SetFlipAxes(FlipAxesArray)
flipFilter.SetInput(rescaler.GetOutput())
flipFilter.Update()
WriteType = itk.ImageFileWriter[OutputImageType]
writer = WriteType.New()
writer.SetFileName(output_file)
writer.SetInput(flipFilter.GetOutput())
writer.Update()
def getAndSaveDRR(dataFolder, rawReaKVFolder, outputFolder):
all_img_vol_names = [x for x in os.listdir(dataFolder) if x.endswith(".nii.gz")]
for i, relative_ct_path in enumerate(all_img_vol_names):
ct_vol_path = os.path.join(dataFolder, relative_ct_path)
drr_img_name = relative_ct_path.rsplit('.', 2)[0]
all_real_kv_names = [x for x in os.listdir(rawReaKVFolder) if x.endswith(".dcm")]
for j, relative_kv_path in enumerate(all_real_kv_names):
kVImgPath = os.path.join(rawReaKVFolder, relative_kv_path)
ds = dicom.dcmread(kVImgPath)
angle = float(ds.get("PositionerPrimaryAngle"))
# read FOV origin
fovOriginX = float(ds.get("FieldOfViewOrigin")[0])
fovOriginY = float(ds.get("FieldOfViewOrigin")[1])
drr_img_rel_path = drr_img_name + '_' + str(angle)
output_drr_path = os.path.join(outputFolder, drr_img_rel_path + ".png")
generateDRRsRTK(input_file=ct_vol_path, output_file=output_drr_path, gantryAngle=angle, projOffsetX=fovOriginX, projOffsetY=fovOriginY)
rawReaKVFolder = 'F:/RAW_REAL_KV_DICOM_132215'
ctDataFolder = 'F:/all_deformed_CTs'
outputDRRFolder = 'F:/all_drrs'
getAndSaveDRR(ctDataFolder, rawReaKVFolder, outputDRRFolder)
import os
import glob
import numpy as np
import itk
import math
from PIL import Image, ImageDraw
import meshio
import matplotlib.pyplot as plt
# Angle in radians betweeen projection central axis and reference axis. [m_ProjectionAngle]
# Source to isocenter (i.e., 3D image center) distance in mm. [m_FocalPointToIsocenterDistance]
# DRR Pixel spacing in DETECTOR plane in mm [im_sx, im_sy]
# We have calculated this value by assuming that the pixel space in the detector plane is 1.0 mm.
# refer https://github.com/InsightSoftwareConsortium/ITKTwoProjectionRegistration/blob/master/test/GetDRRSiddonJacobsRayTracing.cxx#L112-L113
# Size of the DRR image in number of pixels. [dx, dy]
# This function is only use to calculate DRR projection coordinate for only one 3D vertex cooridnate
def getDRRProjectionCoords(ct_img_path=None, points=None, m_ProjectionAngle=0., m_FocalPointToIsocenterDistance=1000., m_Threshold=0.,
source_to_detector_distance=1536., im_sx=0.65104, im_sy=0.65104, dx=1024, dy=1024):
# Use the image center as the central axis position.
# refer https://github.com/InsightSoftwareConsortium/ITKTwoProjectionRegistration/blob/master/test/GetDRRSiddonJacobsRayTracing.cxx#L506-L510
o2Dx = (dx - 1.) / 2.
o2Dy = (dy - 1.) / 2.
# constant for converting degrees into radians
dtr = (math.atan(1.0) * 4.0) / 180.0
Dimension = 3
PointType = itk.Point[itk.D, Dimension]
VectorType = itk.Vector[itk.D, Dimension]
MatrixType = itk.Matrix[itk.D, Dimension, Dimension]
TransformType = itk.Euler3DTransform[itk.D]
# Compute the origin (in mm) of the 2D Image
m_Origin = PointType()
m_Origin[0] = -im_sx * o2Dx
m_Origin[1] = -im_sy * o2Dy
m_Origin[2] = -m_FocalPointToIsocenterDistance
m_Spacing = VectorType()
m_Spacing[0] = im_sx # pixel spacing along X of the 2D DRR image [mm]
m_Spacing[1] = im_sy # pixel spacing along Y of the 2D DRR image [mm]
m_Spacing[2] = 1.0 # slice thickness of the 2D DRR image [mm]
# inverse trasnform to rotate the vertex into DRR detector coordinate system
# user can set an arbitrary transform for the volume. If not explicitly set, it should be identity.
# Here I'm not setting the isocenter location and assume it is in (0, 0, 0) by default,
# but in the DRR algorithm that we used to generate DRR images, Set the center of the 3D image volume (CT) as the isocenter
# refer https://github.com/InsightSoftwareConsortium/ITKTwoProjectionRegistration/blob/master/test/GetDRRSiddonJacobsRayTracing.cxx#L452-L454
m_Transform = TransformType.New()
m_Transform.SetComputeZYX(True)
m_Transform.SetIdentity()
m_InverseTransform = TransformType.New()
m_InverseTransform.SetComputeZYX(True)
m_ComposedTransform = TransformType.New()
m_ComposedTransform.SetComputeZYX(True)
m_GantryRotTransform = TransformType.New()
m_GantryRotTransform.SetComputeZYX(True)
m_GantryRotTransform.SetIdentity()
m_CamShiftTransform = TransformType.New()
m_CamShiftTransform.SetComputeZYX(True)
m_CamShiftTransform.SetIdentity()
m_CamRotTransform = TransformType.New()
m_CamRotTransform.SetComputeZYX(True)
m_CamRotTransform.SetIdentity()
m_CamRotTransform.SetRotation(dtr * (-90.0), 0.0, 0.0)
# isocenter set to (0, 0, 0)
# but in the DRR algorithm that we used to generate DRR images, Set the center of the 3D image volume (CT) as the isocenter
inputImage = itk.imread(ct_img_path)
imOrigin = inputImage.GetOrigin()
imRes = inputImage.GetSpacing()
imRegion = inputImage.GetBufferedRegion()
imSize = imRegion.GetSize()
isocenter = [(imRes[i]*imSize[i]/2.0) for i in range(3)]
m_ComposedTransform.SetIdentity()
m_Transform.SetCenter(isocenter)
m_Transform.SetTranslation([-1.*imOrigin[i] for i in range(3)])
m_ComposedTransform.Compose(m_Transform, False)
# An Euler 3D transform is used to rotate the volume to simulate the roation of the linac gantry.
# The rotation is about z-axis. After the transform, a AP projection geometry (projecting
# towards positive y direction) is established.
m_GantryRotTransform.SetRotation(0.0, 0.0, -1. * dtr * (m_ProjectionAngle))
m_GantryRotTransform.SetCenter(isocenter)
m_ComposedTransform.Compose(m_GantryRotTransform, False)
# An Euler 3D transfrom is used to shift the camera to the detection plane
camtranslation = VectorType()
camtranslation[0] = -isocenter[0]
camtranslation[1] = m_FocalPointToIsocenterDistance - isocenter[1]
camtranslation[2] = -isocenter[2]
#print(camtranslation)
m_CamShiftTransform.SetTranslation(camtranslation)
m_ComposedTransform.Compose(m_CamShiftTransform, False)
# An Euler 3D transform is used to establish the standard negative z-axis projection geometry. (By
# default, the camera is situated at the origin, points down the negative z-axis, and has an up-
# vector of (0, 1, 0))
m_ComposedTransform.Compose(m_CamRotTransform, False)
# The overall inverse transform is computed. The inverse transform will be used by the interpolation
# procedure.
m_ComposedTransform.GetInverse(m_InverseTransform)
MatrixType = itk.Matrix[itk.D, Dimension, Dimension]
m_Direction = MatrixType()
m_Direction.SetIdentity()
m_IndexToPhysicalPoint = MatrixType()
m_IndexToPhysicalPoint.SetIdentity()
m_PhysicalPointToIndex = MatrixType()
m_PhysicalPointToIndex.SetIdentity()
scale = MatrixType()
# Compute helper matrices used to transform Index coordinates to
# PhysicalPoint coordinates and back. This method is virtual and will be
# overloaded in derived classes in order to provide backward compatibility
# behaviour in classes that did not used to take image orientation into
# account.
scale = itk.GetMatrixFromArray(np.diag(m_Spacing))
m_IndexToPhysicalPoint = m_Direction * scale
m_PhysicalPointToIndex = m_IndexToPhysicalPoint.GetInverse()
# this entire bottom part should be iterated over all the points
point_list = points.tolist()
# hold the pixel cooridnates as separate numpy arrays for x and y coordinates
u = np.array([])
v = np.array([])
for point in point_list:
# coordinate to the coordinate system defined by the detector coordinate system
drrPixelWorld = m_ComposedTransform.TransformPoint(point)
#print(drrPixelWorld)
# 2D coordinates on the detector plane (in physical units, not in pixels)
# calculate the physical cooridante in DRR image plane
# i.e. multiply the x and y coordinates by the ratio source-to-detector distance over z
drrPixelWorld[0] = -drrPixelWorld[0] * m_FocalPointToIsocenterDistance/drrPixelWorld[2]
drrPixelWorld[1] = -drrPixelWorld[1] * m_FocalPointToIsocenterDistance/drrPixelWorld[2]
index = VectorType()
for i in range(Dimension):
sum = itk.NumericTraits[itk.D].ZeroValue()
for j in range(Dimension):
sum += m_PhysicalPointToIndex(i, j) * (drrPixelWorld[j] - m_Origin[j])
index[i] = sum
u = np.append(u, index[0])
v = np.append(v, index[1])
return u, v
def getImageDict(img_paths):
img_dict = {}
for i, path in enumerate(img_paths):
key_with_angle = os.path.basename(path).rsplit('.', 1)[0]
# key hold the volumetric mesh name (i.e. ellipsoid) by removing the gantry angle (i.e. ellipsoid_72)
key = os.path.basename(key_with_angle).rsplit('_', 1)[0]
if key in img_dict:
img_dict[key].append(path)
else:
img_dict[key] = [path]
return img_dict
def alignedWithDRR(ct_img_path, meshPaths, img_dict, outputImgFolder):
for j, path in enumerate(meshPaths):
mesh = meshio.read(path)
img_key = os.path.basename(path).rsplit('.', 1)[0]
drr_img_path_list = img_dict[img_key]
for drr_img_path in drr_img_path_list:
# extract the drr image name with extention from the absolute path (e.g. volume-10_0.png)
img_name_str = os.path.basename(drr_img_path).rsplit('.', 1)[0]
# projection angle in degrees
gantry_angle = float(os.path.basename(img_name_str).rsplit('_', 1)[1])
print('gantry_angle:', str(gantry_angle))
img_name = img_name_str + '_aligned'
print('image_name:', img_name)
# need to pass projection angle to get the 2D coordinates for each case
u, v = getDRRProjectionCoords(ct_img_path=ct_img_path, points=mesh.points, m_ProjectionAngle=gantry_angle)
v = 1023-v # Mimicks flipFilter
u = u.tolist()
v = v.tolist()
output_img_path = os.path.join(outputImgFolder, img_name + ".png")
i = Image.open(drr_img_path).convert("RGBA")
draw = ImageDraw.Draw(i)
w, h = i.size
# plot U, V points
for x, y in zip(u, v):
draw.point((x, y), fill="red")
i.save(output_img_path)
ct_img_path = 'F:/all_CTs_HU_corrected/liver_ct.nii.gz'
mesh_paths = glob.glob('F:/meshes/*.vtk')
drr_img_paths = glob.glob('F:/ITK_DRRs/all_drrs/*.png')
output_img_folder = 'F:/results_proejctions'
img_dict = getImageDict(drr_img_paths)
alignedWithDRR(ct_img_path, mesh_paths, img_dict, output_img_folder)
import os
import math
import numpy as np
import itk
# <-res float float> DRR Pixel spacing in isocenter plane in mm {im_sx, im_sy}
# <-size int int> Size of DRR in number of pixels [default: 512x512] {dx, dy}
# <-scd float> Source to isocenter (i.e., 3D image center) distance in mm [default: 1000mm]
# <-t float float float> CT volume translation in x, y, and z direction in mm {tx, ty, tz}
# <-rx float> CT volume rotation about x axis in degrees
# <-ry float> CT volume rotation about y axis in degrees
# <-rz float> CT volume rotation about z axis in degrees
# <-2dcx float float> Central axis position of DRR in continuous indices {o2Dx, o2Dy}
# <-iso float float float> Continous voxel indices of CT isocenter (center of rotation and projection center) {cx, cy, cz}
# <-rp float> Projection angle in degrees {rprojection}
# <-threshold float> CT intensity threshold, below which are ignored [default: 0]
def siddonJacobRayTracingDRR(input_name=None, output_name=None, rprojection=0., rx=0., ry=0., rz=0., tx=0., ty=0., tz=0.,
cx=0., cy=0., cz=0., threshold=0., scd=1000, im_sx=0.65104, im_sy=0.65104,
dx=512, dy=512, o2Dx=0.0, o2Dy=0.0):
Dimension = 3
InputPixelType = itk.F
OutputPixelType = itk.UC
InputImageType = itk.Image[InputPixelType, Dimension]
OutputImageType = itk.Image[OutputPixelType, Dimension]
ReaderType = itk.ImageFileReader[InputImageType]
reader = ReaderType.New()
reader.SetFileName(input_name)
reader.Update()
inputImage = reader.GetOutput()
# To simply Siddon-Jacob's fast ray-tracing algorithm, we force the origin of the CT image
# to be (0,0,0). Because we align the CT isocenter with the central axis, the projection
# geometry is fully defined. The origin of the CT image becomes irrelavent.
ctOrigin = [0.0, 0.0, 0.0]
inputImage.SetOrigin(ctOrigin)
FilterType = itk.ResampleImageFilter[InputImageType, InputImageType]
filter = FilterType.New()
filter.SetInput(inputImage)
filter.SetDefaultPixelValue(0)
# An Euler transformation is defined to position the input volume.
TransformType = itk.Euler3DTransform[itk.D]
transform = TransformType.New()
transform.SetComputeZYX(True)
translation = [tx, ty, tz]
# converting degrees into radians old val 1./180.*math.pi
dtr = (math.atan(1.0) * 4.0) / 180.0
transform.SetTranslation(translation)
transform.SetRotation(dtr * rx, dtr * ry, dtr * rz)
imOrigin = inputImage.GetOrigin()
imRes = inputImage.GetSpacing()
imRegion = inputImage.GetBufferedRegion()
imSize = imRegion.GetSize()
isocenter = [imOrigin[i] + (imRes[i]*imSize[i]/2.0) for i in range(3)]
transform.SetCenter(isocenter)
InterpolatorType = itk.SiddonJacobsRayCastInterpolateImageFunction[InputImageType, itk.D]
interpolator = InterpolatorType.New()
interpolator.SetProjectionAngle(dtr * rprojection) # Set angle between projection central axis and -z axis
interpolator.SetFocalPointToIsocenterDistance(scd) # Set source to isocenter distance
interpolator.SetThreshold(threshold) # Set intensity threshold, below which are ignored.
interpolator.SetTransform(transform)
interpolator.Initialize()
filter.SetInterpolator(interpolator)
# number of pixels of the 2D DRR image
size = [dx, dy, 1]
# pixel spacing of the 2D DRR image [mm]
spacing = [im_sx, im_sy, 1]
filter.SetSize(size)
filter.SetOutputSpacing(spacing)
# Use the image centers as the central axis position.
o2Dx = (dx - 1.) / 2.
o2Dy = (dy - 1.) / 2.
# Compute the origin (in mm) of the 2D Image
origin = [-im_sx * o2Dx, -im_sy * o2Dy, -scd]
filter.SetOutputOrigin(origin)
filter.Update()
RescaleType = itk.RescaleIntensityImageFilter[InputImageType, OutputImageType]
rescaler = RescaleType.New()
rescaler.SetOutputMinimum(0)
rescaler.SetOutputMaximum(255)
rescaler.SetInput(filter.GetOutput())
rescaler.Update()
# Out of some reason, the computed projection is upsided-down.
# Here we use a FilpImageFilter to flip the images in y direction.
FlipFilterType = itk.FlipImageFilter[OutputImageType]
flipFilter = FlipFilterType.New()
FlipAxesArray = itk.FixedArray[itk.D, 3]()
FlipAxesArray[0] = 0
FlipAxesArray[1] = 1
FlipAxesArray[2] = 0
flipFilter.SetFlipAxes(FlipAxesArray)
flipFilter.SetInput(rescaler.GetOutput())
flipFilter.Update()
WriteType = itk.ImageFileWriter[OutputImageType]
writer = WriteType.New()
writer.SetFileName(output_name)
writer.SetInput(flipFilter.GetOutput())
writer.Update()
def generateAndSaveDRR(dataFolder, outputFolder, numberOfProjections):
firstAngle = 0
angularArc = 360
all_img_vol_names = [x for x in os.listdir(dataFolder) if x.endswith(".nii.gz")]
for i, relative_path in enumerate(all_img_vol_names):
img_vol_path = os.path.join(dataFolder, relative_path)
drr_img_name = relative_path.rsplit('.', 2)[0]
for noProj in range(numberOfProjections):
angle = firstAngle + noProj * angularArc / numberOfProjections
drr_img_rel_path = drr_img_name + '_' + str(int(angle))
output_drr_Path = os.path.join(outputFolder, drr_img_rel_path + ".png")
siddonJacobRayTracingDRR(input_name=img_vol_path, output_name=output_drr_Path, rprojection=angle)
dataFolder = 'F:/all_CTs_HU_corrected'
outputFolder = 'F:/ITK_DRRs/all_drrs'
generateAndSaveDRR(dataFolder, outputFolder, 10)
_______________________________________________
Rtk-users mailing list
rtk-us...@openrtk.org
https://www.creatis.insa-lyon.fr/mailman/listinfo/rtk-users