Skip to main content

Cone Beam Reconstruction and Void Analysis Workflow

Workflows 📥 Download Script

Automate CT Analysis in Volvicon: Reconstruction, Void Analysis, Reports.

This tutorial walks through the automated workflow for cone beam CT reconstruction, void analysis, and final report generation. It provides a step-by-step overview of how to process projection data, evaluate reconstructed volumes, and produce an analysis report.

Prerequisites

  • The Volvicon application must be running
  • Sample projection files suitable for cone beam reconstruction

Scripting Api Initialization​

import ScriptingApi as api
import os

# Create Application instance
app = api.Application()

# Closing the current project, if there is one
app.close_project()

# Get the volume and analysis operations
volume_operations = app.get_volume_operations()
analysis_operations = app.get_analysis_operations()

Cone Beam CT Reconstruction​

# Reconstruct 3D volume from cone beam CT projection images using FDK algorithm
#
# GPU acceleration: reconstruction automatically runs on a supported NVIDIA GPU
# (via CUDA) when one is available and otherwise falls back to the CPU, so this
# script behaves the same on any machine. GPU acceleration needs an NVIDIA Turing
# (RTX 20-series) or newer GPU (Ampere / RTX 30-series recommended) and NVIDIA
# driver R610 or later; older GPUs or drivers automatically use the CPU. The
# behaviour is controlled application-wide from File > Preferences > General >
# Performance > "GPU acceleration (NVIDIA CUDA)".

# Helper function to create file paths with zero-padded numbering
def create_file_paths(directory, prefix, start, end, num_zeros, extension='.tif'):
file_paths = []
for i in range(start, end + 1):
padded_number = str(i).zfill(num_zeros)
filename = f"{prefix}{padded_number}{extension}"
full_path = os.path.join(directory, filename)
file_paths.append(full_path)
return file_paths

projection_files = create_file_paths(
directory="C:/Samples/Projections",
prefix="FewView_Object",
start=100001,
end=101001,
num_zeros=6
)

# Print the generated file paths (for verification)
for path in projection_files:
print(path)

# Configure reconstruction settings
cone_beam_reconstruction_settings = api.ConeBeamReconstructionSettings()

# Inversion settings (optional, for correcting projection orientation)
cone_beam_reconstruction_settings.invert_x = False # Mirror horizontally
cone_beam_reconstruction_settings.invert_y = False # Mirror vertically
cone_beam_reconstruction_settings.invert_z = False # Reverse projection order
cone_beam_reconstruction_settings.invert_intensity = False # Invert intensity values

# Geometry parameters (critical for accurate reconstruction)
cone_beam_reconstruction_settings.source_to_detector_distance = 815.19018878 # Distance from X-ray source to detector (mm)
cone_beam_reconstruction_settings.source_to_isocenter_distance = 153.48085 # Distance from source to object center (mm)
cone_beam_reconstruction_settings.detector_pixel_size_x = 0.4 # Detector pixel width (mm)
cone_beam_reconstruction_settings.detector_pixel_size_y = 0.4 # Detector pixel height (mm)
cone_beam_reconstruction_settings.xray_scan_start_angle = 0.0 # Starting scan angle (degrees)
cone_beam_reconstruction_settings.xray_scan_total_angle = 360.0 # Total scan arc (degrees, 360° for full scan)
cone_beam_reconstruction_settings.is_clockwise = False # Rotation direction (False = anti-clockwise)

# Projection offset parameters (for detector misalignment correction)
cone_beam_reconstruction_settings.projection_offset_x = 0.0 # Horizontal detector offset (mm)
cone_beam_reconstruction_settings.projection_offset_y = 0.0 # Vertical detector offset (mm)
cone_beam_reconstruction_settings.source_offset_x = 0.0 # Horizontal source offset (mm)
cone_beam_reconstruction_settings.source_offset_y = 0.0 # Vertical source offset (mm)
cone_beam_reconstruction_settings.out_of_plane_angle = 0.0 # Detector tilt around X axis (degrees)
cone_beam_reconstruction_settings.in_plane_angle = 0.0 # Detector tilt around Z axis (degrees)

# Displaced detector settings (for short scans)
cone_beam_reconstruction_settings.enable_displaced_detector = False # Enable for off-center detector
cone_beam_reconstruction_settings.angular_gap_threshold = 20.0 # Gap threshold for short scan detection (degrees)

# Automatic scan geometry correction: estimates the horizontal detector offset
# (center of rotation) from opposing projections and replaces projection_offset_x.
# Requires a scan arc of at least 180 degrees.
cone_beam_reconstruction_settings.enable_auto_detector_offset_x = False

# Preprocessing parameters
cone_beam_reconstruction_settings.enable_logarithm_conversion = True # Apply logarithmic transformation

# Reconstruction filter parameters
cone_beam_reconstruction_settings.truncation_correction = 1.0 # Detector-width fraction feathered (0.0-1.0, 0 = off)
cone_beam_reconstruction_settings.reconstruction_filter = api.CBR_ReconstructionFilter.RamLak # RamLak/SheppLogan/Hann/Hamming
cone_beam_reconstruction_settings.filter_cut_frequency = 0.0 # Cut frequency (0.0-1.0, 0 = pure ramp, lower = smoother)

# Post-processing parameters
cone_beam_reconstruction_settings.auto_invert_intensity = False # Auto-invert if contrast is reversed
cone_beam_reconstruction_settings.auto_crop = True # Auto-crop to object bounds
cone_beam_reconstruction_settings.crop_margin = 10 # Margin around cropped object (pixels)

# Noise reduction on the reconstructed volume (runs before sharpening).
# This example applies a separate bilateral filter after reconstruction instead.
cone_beam_reconstruction_settings.noise_reduction_mode = api.CBR_NoiseReductionMode.Off # Off/Median/Gaussian/EdgePreserving
cone_beam_reconstruction_settings.noise_reduction_level = api.CBR_NoiseReductionLevel.Medium # Low/Medium/High

# Histogram contrast windowing (percentile-based outlier clipping and stretching)
cone_beam_reconstruction_settings.enable_histogram_windowing = True # Improve the contrast distribution
cone_beam_reconstruction_settings.histogram_windowing_margin_percent = 5.0 # Margin so the clip does not cut tightly (percent)

# Sharpening parameters (Unsharp Mask)
cone_beam_reconstruction_settings.enable_sharpening = False # Disable USM sharpening
cone_beam_reconstruction_settings.sharpening_iterations = 2 # Number of sharpening passes (1-5)
cone_beam_reconstruction_settings.sharpening_radius = 3 # Sharpening radius (1-5 pixels)
cone_beam_reconstruction_settings.sharpening_contrast = 50 # Sharpening amount (10-200 percent)

# Example 1: Reconstruct from projection files on disk
reconstructed_volume = volume_operations.reconstruct_volume_using_cone_beam_reconstruction(
"", # volumeName: empty string when using files
projection_files, # fileNames: list of projection file paths
cone_beam_reconstruction_settings # reconstruction parameters
)

print(f"Reconstructed volume from files: {reconstructed_volume}")

volume_operations.bilateral_filter([reconstructed_volume], 1, 1, 1, 8000, 2)

volume_render_properties_operations = volume_operations.get_render_properties_operations()
volume_render_properties_operations.set_blend_mode([reconstructed_volume], 0)
volume_render_properties_operations.set_shade_enabled([reconstructed_volume], True)

Void / Inclusion Analysis Workflow​

void_inclusion_params = api.VoidInclusionParams()
void_inclusion_params.mode = api.VoidInclusionTargetType.Void
void_inclusion_params.method = api.VoidInclusionMethod.Absolute
void_inclusion_params.auto_absolute_contrast = True
void_inclusion_params.requested_statistics = [
"Volume (mm³)",
"Volume fraction (%)",
"Centroid X (mm)",
"Centroid Y (mm)",
"Centroid Z (mm)",
"Compactness",
"Sphericity",
"Equivalent diameter (mm)"
]

void_inclusion_filtering_params = api.VoidInclusionFilteringParams()
void_inclusion_filtering_params.enable_geometric_filtering = True
void_inclusion_filtering_params.probability_threshold = 0.0
void_inclusion_filtering_params.min_voxel_count = 27
void_inclusion_filtering_params.max_voxel_count = 10000000
void_inclusion_filtering_params.min_compactness = 0.0
void_inclusion_filtering_params.max_compactness = 1.0
void_inclusion_filtering_params.min_sphericity = 0.0
void_inclusion_filtering_params.max_sphericity = 1.0
void_inclusion_filtering_params.min_volume_fraction_percent = 0.0
void_inclusion_filtering_params.max_volume_fraction_percent = 100.0
void_inclusion_filtering_params.min_equivalent_diameter = 0.0
void_inclusion_filtering_params.max_equivalent_diameter = 1000000.0
void_inclusion_filtering_params.max_count = 100

void_inclusion_params.filter_result_params = void_inclusion_filtering_params

vi_name = analysis_operations.create_void_inclusion_analysis(
"VoidInclusion_1",
reconstructed_volume,
"", # No ROI mask
void_inclusion_params
)

# B. Running
if analysis_operations.run_analysis(vi_name):
print(f"Analysis run successfully: {vi_name}")
all_masks = app.get_all_mask_names()
if all_masks:
last_mask = all_masks[-1]
app.isolate_masks([last_mask])
app.set_mask_3d_preview_quality(api.Mask3dPreviewQuality.Optimal)
app.generate_mask_3d_preview(app.get_visible_mask_names())

app.set_volumes_visible([reconstructed_volume], False)

# C. Getting Info and Results
analysis_info : api.AnalysisInfo = analysis_operations.get_analysis_info(vi_name)
print(f"Analysis info: name={analysis_info.name}, type={analysis_info.type}")

# Get defect results
void_inclusion_results : api.VoidInclusionResults = analysis_operations.get_void_inclusion_results(vi_name)
print(f"Void/Inclusion Stats:")
print(f" Total defects: {void_inclusion_results.total_defects_found}")
print(f" Total defect volume: {void_inclusion_results.total_defect_volume}")
print(f" Porosity: {void_inclusion_results.porosity}")
print(f" Label statistics count: {len(void_inclusion_results.label_statistics)}")

# Get descriptive statistics
print(f"descriptive_statistics_count: {len(void_inclusion_results.descriptive_statistics)}")
for item in void_inclusion_results.descriptive_statistics:
descriptive_statistic : api.VoidInclusionDescriptiveStatistic = item
stat_type = descriptive_statistic.statistic_type
std_dev = descriptive_statistic.std_deviation
print(f"{stat_type}: {std_dev}")

# Get label statistics
print(f"label_statistics_count: {len(void_inclusion_results.label_statistics)}")
for item in void_inclusion_results.label_statistics:
label_statistics : api.VoidInclusionLabelStatistics = item
label = label_statistics.label
for stat_item in label_statistics.statistics:
statistic : api.VoidInclusionStatistic = stat_item
stat_type = statistic.statistic_type
value = statistic.values[0] if statistic.values else None
print(f"{label} | {stat_type}: {value}")

# Get whole-mask statistics
print(f"whole_mask_statistics_count: {len(void_inclusion_results.whole_mask_statistics)}")
for item in void_inclusion_results.whole_mask_statistics:
whole_mask_statistic : api.VoidInclusionWholeMaskStatistic = item
value = whole_mask_statistic.values[0] if whole_mask_statistic.values else None
print(f"{whole_mask_statistic.statistic_type}: {value}")

# D. Writing Results to Disk
output_dir = r'C:\Samples\Output'
os.makedirs(output_dir, exist_ok=True)
analysis_operations.write_void_inclusion_results_to_disk(void_inclusion_results, os.path.join(output_dir, 'void_inclusion.txt'))

# E. Changing Display Settings
analysis_display_settings = api.AnalysisDisplaySettings()
analysis_display_settings.lookup_table_type = api.LookupTableType.ReverseRainbow
analysis_operations.set_display_settings(vi_name, analysis_display_settings)

# F. Updating Analysis
analysis_operations.update_analysis(vi_name)

Post-Analysis Operations​

# Save snapshots and the project to disk, then close the project to free up memory

app.save_snapshot_to_disk(api.SnapshotType.Application, 'C:/Samples/Application.png')
app.save_snapshot_to_disk(api.SnapshotType.Scene, 'C:/Samples/Scene.png')
app.save_snapshot_to_disk(api.SnapshotType.View3D, 'C:/Samples/View3D.png')

app.save_project("C:/Samples/cbct-workflow.vvcx")

app.close_project()

print("Analysis operations completed successfully.")