Inline Reconstruction¶
InlineReconstruction demonstrates how to perform cone-beam CT reconstruction using RTK’s implementation of FDK while projections are being acquired.
The program simulates the acquisition of X-ray projections in a separate thread,
gradually reconstructing the 3D volume as new projections become available.
#include <iostream>
#include <thread>
#include <chrono>
#include <vector>
#include <iomanip>
#include <sstream>
#include <atomic>
#include <itkImageFileWriter.h>
#include <itkExtractImageFilter.h>
#include <rtkFDKConeBeamReconstructionFilter.h>
#include <rtkParkerShortScanImageFilter.h>
#include <rtkProjectionsReader.h>
#include <rtkSheppLoganPhantomFilter.h>
#include <rtkConstantImageSource.h>
// CUDA headers (if available)
#ifdef RTK_USE_CUDA
# include <rtkCudaFDKConeBeamReconstructionFilter.h>
# include <rtkCudaParkerShortScanImageFilter.h>
# include <itkCudaImage.h>
#endif
// Type definitions
using ImageType = itk::Image<float, 3>;
using CudaImageType = itk::CudaImage<float, 3>;
// Constants for acquisition and reconstruction
const int numProjections = 64;
const double sid = 1000.0;
const double sdd = 1500.0;
const double arc = 200.0;
const double spacing = 1.0;
const int imageSize = 256;
const double origin = -0.5 * (imageSize - 1);
const std::chrono::milliseconds acquisitionSleepDuration(100);
std::atomic<int> AcquiredProjectionsCount(0);
// Function for the thread simulating an acquisition
// Simulates and writes a projection every 100 ms.
void
Acquisition()
{
for (int i = 0; i < numProjections; ++i)
{
// Create geometry for the acquisition
auto geometryAcq = rtk::ThreeDCircularProjectionGeometry::New();
geometryAcq->AddProjection(sid, sdd, i * arc / numProjections);
// Set the origin for the projection
itk::Point<double, 3> point;
point[0] = origin;
point[1] = origin;
point[2] = 0.0;
// Create a constant image source for the projection
auto projection = rtk::ConstantImageSource<ImageType>::New();
projection->SetOrigin(point);
projection->SetSize({ imageSize, imageSize, 1 });
// Set spacing for the projection
itk::Vector<double, 3> spacingVec;
spacingVec.Fill(spacing);
projection->SetSpacing(spacingVec);
projection->Update();
// Create Shepp-Logan phantom filter
auto phantom = rtk::SheppLoganPhantomFilter<ImageType, ImageType>::New();
phantom->SetInput(projection->GetOutput());
phantom->SetGeometry(geometryAcq);
phantom->SetPhantomScale(100);
// Create filename for the output projection
std::ostringstream filename;
filename << "projection_" << std::setw(3) << std::setfill('0') << i << ".mha";
phantom->Update();
// Write the projection image to file
itk::WriteImage(phantom->GetOutput(), filename.str());
AcquiredProjectionsCount++;
std::this_thread::sleep_for(acquisitionSleepDuration);
}
}
int
main()
{
// Launch the simulated acquisition
std::thread acquisitionThread(Acquisition);
// Create the geometry for reconstruction
auto geometryRec = rtk::ThreeDCircularProjectionGeometry::New();
for (int i = 0; i < numProjections; ++i)
{
geometryRec->AddProjection(sid, sdd, i * arc / numProjections);
}
// Create the reconstruction pipeline
auto reader = rtk::ProjectionsReader<ImageType>::New();
auto extractor = itk::ExtractImageFilter<ImageType, ImageType>::New();
extractor->SetInput(reader->GetOutput());
// Set the origin for the reconstruction
itk::Point<double, 3> originPoint;
originPoint.Fill(origin * sid / sdd);
// Set spacing for the reconstruction
itk::Vector<double, 3> spacingVec;
spacingVec.Fill(spacing * sid / sdd);
#ifdef RTK_USE_CUDA
// Use CUDA for Parker short scan image filter
auto parker = rtk::CudaParkerShortScanImageFilter::New();
parker->SetGeometry(geometryRec);
auto reconstructionSource = rtk::ConstantImageSource<CudaImageType>::New();
reconstructionSource->SetOrigin(originPoint);
reconstructionSource->SetSpacing(spacingVec);
reconstructionSource->SetSize({ imageSize, imageSize, imageSize });
auto fdk = rtk::CudaFDKConeBeamReconstructionFilter::New();
#else
// Use CPU for Parker short scan image filter
auto parker = rtk::ParkerShortScanImageFilter<ImageType>::New();
parker->SetInput(extractor->GetOutput());
parker->SetGeometry(geometryRec);
auto reconstructionSource = rtk::ConstantImageSource<ImageType>::New();
reconstructionSource->Setorigin(originPoint);
reconstructionSource->Setspacing(spacingVec);
reconstructionSource->SetSize({ imageSize, imageSize, imageSize });
auto fdk = rtk::FDKConeBeamReconstructionFilter<ImageType>::New();
#endif
fdk->SetGeometry(geometryRec);
fdk->SetInput(0, reconstructionSource->GetOutput());
fdk->SetInput(1, parker->GetOutput());
// Online reconstruction loop
int reconstructedProjectionsCount = 0;
std::vector<std::string> projectionFileNames(numProjections, "projection_000.mha");
while (reconstructedProjectionsCount != numProjections)
{
if (reconstructedProjectionsCount < AcquiredProjectionsCount)
{
std::cout << "Processing projection #" << reconstructedProjectionsCount << "\r";
if (reconstructedProjectionsCount == 0)
{
reader->SetFileNames(projectionFileNames);
reader->UpdateOutputInformation();
}
else
{
// Create filename for the next projection
std::ostringstream filename;
filename << "projection_" << std::setw(3) << std::setfill('0') << reconstructedProjectionsCount << ".mha";
projectionFileNames[reconstructedProjectionsCount] = filename.str();
reader->SetFileNames(projectionFileNames);
// Disconnect the pipeline to allow for proper reconstruction
ImageType::Pointer reconstructedImage = fdk->GetOutput();
reconstructedImage->DisconnectPipeline();
fdk->SetInput(0, reconstructionSource->GetOutput());
}
// Extract and read the new projection
auto extractedRegion = reader->GetOutput()->GetLargestPossibleRegion();
extractedRegion.SetIndex(2, reconstructedProjectionsCount);
extractedRegion.SetSize(2, 1);
extractor->SetExtractionRegion(extractedRegion);
extractor->UpdateLargestPossibleRegion();
#ifdef RTK_USE_CUDA
// Prepare projection for CUDA processing
auto projection = CudaImageType::New();
projection->SetPixelContainer(extractor->GetOutput()->GetPixelContainer());
projection->CopyInformation(extractor->GetOutput());
projection->SetBufferedRegion(extractor->GetOutput()->GetBufferedRegion());
projection->SetRequestedRegion(extractor->GetOutput()->GetRequestedRegion());
parker->SetInput(projection);
#endif
fdk->Update();
reconstructedProjectionsCount++;
}
else
{
// Sleep briefly to avoid busy waiting
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
acquisitionThread.join();
itk::WriteImage(fdk->GetOutput(), "fdk.mha");
return 0;
}
import sys
import os
import time
import threading
import itk
from itk import RTK as rtk
# Parameters controlling the geometry of the simulated acquisition
image_type=itk.Image[itk.F,3]
has_gpu_capability=hasattr(itk, 'CudaImage')
if has_gpu_capability:
cuda_image_type=itk.CudaImage[itk.F,3]
nproj=64
sid=1000
sdd=1500
arc=200
spacing=1
size=256
origin=-0.5*(size-1)
number_of_acquired_projections = 0
# Function for the thread simulating an acquisition: simulates and writes a
# projection every 100 ms.
def acquisition():
global number_of_acquired_projections
for i in range(nproj):
geometry_acq = rtk.ThreeDCircularProjectionGeometry.New()
geometry_acq.AddProjection(sid, sdd, i*arc/nproj)
projection = rtk.constant_image_source(origin=[origin,origin,0.], size=[size,size,1], spacing=[spacing]*3, ttype=image_type)
projection = rtk.shepp_logan_phantom_filter(projection, geometry=geometry_acq, phantom_scale=100, )
itk.imwrite(projection, f'projection_{i:03d}.mha')
# The code assumes that incrementing an integer is thread safe, which
# is the case with CPython
# https://docs.python.org/3/library/threading.html
number_of_acquired_projections += 1
time.sleep(.1)
# Main program: launches the acquition thread, then waits for new projections an reconstructs inline / on-the-fly
if __name__ == "__main__":
# Launch the simulated acquisition
thread = threading.Thread(target = acquisition)
thread.start()
# Create the (expected) geometry and filenames
geometry_rec = rtk.ThreeDCircularProjectionGeometry.New()
for i in range(nproj):
geometry_rec.AddProjection(sid, sdd, i*arc/nproj)
# Create the reconstruction pipeline
reader = rtk.ProjectionsReader[image_type].New()
extractor = itk.ExtractImageFilter[image_type, image_type].New(Input=reader.GetOutput())
if has_gpu_capability:
parker = rtk.CudaParkerShortScanImageFilter.New(Geometry=geometry_rec)
reconstruction_source = rtk.ConstantImageSource[cuda_image_type].New(Origin=[origin*sid/sdd]*3, Spacing=[spacing*sid/sdd]*3, Size=[size]*3)
fdk = rtk.CudaFDKConeBeamReconstructionFilter.New(Geometry=geometry_rec)
else:
parker = rtk.ParkerShortScanImageFilter[image_type].New(Input=extractor.GetOutput(), Geometry=geometry_rec)
reconstruction_source = rtk.ConstantImageSource[image_type].New(Origin=[origin*sid/sdd]*3, Spacing=[spacing*sid/sdd]*3, Size=[size]*3)
fdk = rtk.FDKConeBeamReconstructionFilter[image_type].New(Geometry=geometry_rec)
fdk.SetInput(0, reconstruction_source.GetOutput())
fdk.SetInput(1, parker.GetOutput())
# Do the online / on-the-fly reconstruction: wait for acquired projections
# and use them as soon as a new one is available
number_of_reconstructed_projections = 0
while number_of_reconstructed_projections != nproj:
new_projection_available = bool(number_of_reconstructed_projections<number_of_acquired_projections)
if not new_projection_available:
time.sleep(.01)
else:
print(f'Processing projection #{number_of_reconstructed_projections}', end='\r')
if number_of_reconstructed_projections==0:
# First projection, mimick a stack from one file and prepare extracted region
projection_file_names = ['projection_000.mha']*nproj
reader.SetFileNames(projection_file_names)
reader.UpdateOutputInformation()
extracted_region = reader.GetOutput().GetLargestPossibleRegion()
extracted_region.SetSize(2, 1)
else:
# Update file name list with the new projection
projection_file_names[number_of_reconstructed_projections] = f'projection_{number_of_reconstructed_projections:03d}.mha'
reader.SetFileNames(projection_file_names)
# Reconnect FDK output to
reconstructed_image = fdk.GetOutput()
reconstructed_image.DisconnectPipeline()
fdk.SetInput(reconstructed_image)
# Only extract and read the new projection
extracted_region.SetIndex(2, number_of_reconstructed_projections)
extractor.SetExtractionRegion(extracted_region)
extractor.UpdateLargestPossibleRegion()
if has_gpu_capability:
projection = cuda_image_type.New()
projection.SetPixelContainer(extractor.GetOutput().GetPixelContainer())
projection.CopyInformation(extractor.GetOutput())
projection.SetBufferedRegion(extractor.GetOutput().GetBufferedRegion())
projection.SetRequestedRegion(extractor.GetOutput().GetRequestedRegion())
parker.SetInput(projection)
fdk.Update()
number_of_reconstructed_projections += 1
thread.join()
writer = itk.ImageFileWriter[image_type].New(Input=fdk.GetOutput(), FileName='fdk.mha')
writer.Update()