8. Simulated X-ray imaging with GVXR#

8.1. Introduction#

X-ray imaging is a common to method used to to perform detailed analyses of the internal structure of an object in non-destructive way. VirtualLab allows users to create realistic simulations of X-Ray images using the software package GVXR. GVXR is a C++ x-ray simulation library developed by Frank Vidal and Iwan Mitchel at Bangor University.

It uses ray-tracing in OpenGL to track the path and attenuation of X-ray beams through a polygon mesh as they travel from an X-Ray source to a detector.

This tutorial will not cover the specifics of how to use GVXR, however training material on this in the form of jupiter notebooks can be found here.

The goal here instead is to show how GVXR can be run as a method inside a container within the VirtualLab workflow. As such we will cover similar examples to the training material but not the detailed theory behind them.

8.2. Prerequisites#

The examples provided here are mostly self-contained. However, in order to understand this tutorial, at a minimum you will need to have completed the first tutorial to obtain a grounding in how VirtualLab is setup.

Also, although not strictly necessary, we also recommend completing the third tutorial because we will be using the Salome mesh generated from the HIVE analysis as part of one of the examples. All the previous tutorials (that is tutorials 2, 4 and 5) are useful but not required if your only interest is the X-Ray imaging features.

We also recommend you have at least some understanding of how to use GVXR as a standalone package and have looked through the GVXR training material as we will be working through very similar examples.

8.3. Example 1: First X-ray Simulations with GVXR#

In this first example we will demonstrate how to simulate a single X-Ray image. We will start with a simple monochromatic point source and an object made from a single element.

Action

The RunFile RunTutorials.py should be set up as follows to run this simulation:

Simulation='Examples'
Project='Dragon'
Parameters_Master='TrainingParameters_GVXR-Draig'
Parameters_Var=None

#===============================================================================
# Environment
#===============================================================================

VirtualLab=VLSetup(
        Simulation,
        Project
        )

VirtualLab.Settings(
        Mode='Interactive',
        Launcher='Process',
        NbJobs=1
        )

VirtualLab.Parameters(
        Parameters_Master,
        Parameters_Var,
        RunCT_Scan=True,
        RunCT_Recon=False
        )

VirtualLab.CT_Scan()

A copy of this run file can also be found in RunFiles/Tutorials/X-ray_imaging/Task1_Run.py.

The mesh we be using For this example is the Welsh Dragon Model which was released by Bangor university, UK, for Eurographics 2011. The mesh, which can be found here, can be downloaded and placed in the directory Output/Examples/Dragon/Meshes by running the following command

VirtualLab -f RunFiles/Tutorials/X-ray_imaging/Task1_mesh.py

Action

Once you have the mesh downloaded you can launch VirtualLab using the following command:

VirtualLab -f RunFiles/RunTutorials.py

Because we have set Mode='Interactive' in VirtualLab.Settings you should see a 3D visualization of the dragon model in the path of the X-Ray beam casting a shadow onto the X-Ray detector behind, see Fig. 8.1.

You can use the mouse to zoom and rotate the scene to get a better view. Once finished you can close the window or type q on the keyboard.

Tip

To prevent this visualization from appearing in future runs simply set Mode to 'Headless' or 'Terminal'.

https://gitlab.com/ibsim/media/-/raw/master/images/docs/screenshots/GVXR_Dragon_1.png

Fig. 8.1 Visualization of X-Ray imaging for Dragon model#

The X-ray image itself can be found in Output/GVXR/Tutorials/GVXR_Images/Dragon/Dragon_1.tiff, and should look like Fig. 8.2.

https://gitlab.com/ibsim/media/-/raw/master/images/docs/screenshots/GVXR_Dragon_2.png

Fig. 8.2 X-Ray Image of Dragon model.#

Looking though the RunFile The main thing to note is the call to VirtualLab.CT_Scan(). This is the function that initiates X-ray imaging using the parameters defined in Parameters_Master and Parameters_Var. Additionally, RunCT_Scan is explicitly set to True in VirtualLab.Parameters.

This isn’t technically necessary because the inclusion of VirtualLab.CT_Scan() in the methods section means it is True by default, but explicitly stating this is good practice.

The parameters file we used is Input/Examples/Dragon/TrainingParameters_GVXR-Draig.py you will notice this file has a new Namespace GVXR. This contains the parameters used to setup and control the X-Ray Imaging. The file is setup with some sensible default values.

The GVXR Namespace contains a number of options many of which we will cover in later examples. For the curious a full list of these can be found in the appendix.

For ease of discussion of this first example we will break the required parameters down into four sections:

  1. X-ray Beam parameters

  2. Detector Parameters

  3. Sample Parameters

  4. Misc. Parameters

8.3.1. Setting up the Beam:#

Our first group of parameters concern the properties of the X-Ray Beam (source) GVXR needs to know 3 basic properties to define a source.

  1. The position of the source

  2. The beam shape

  3. The beam energy (spectrum)

To set the position we use GVXR.Beam_PosX, GVXR.Beam_PosY and GVXR.Beam_PosZ the default units are mm. However, you can easily change this to essentially any metric units by setting GVXR.Beam_Pos_units to the appropriate string (“mm”,”cm”,”m” etc …)[1]_.

For the beam shape we use GVXR.Beam_Type. GVXR allows for two choices:

  • Cone beam: GVXR.Beam_Type = 'point'

  • Parallel beam (e.g. synchrotron): GVXR.Beam_Type = 'parallel'

Finally we need to set the beam spectrum. Out of the box GVXR supports Monochromatic and PolyChromatic sources. You can also use the package xpecgen to generate more realistic/complex spectra, such as those from xray tubes. This will be covered in a later session. For now we will stick with a simple Monochromatic source.

This can be set with GVXR.Energy, this should be floating point (decimal) number, default units are MeV. The Intensity (taken as number of photons) is set with GVXR.Intensity this should be an integer (whole number). You can also optionally use GVXR.energy_units with a string to denote the energy units. This can be any of “eV”, “keV” or “MeV” (take care with capitalization).

Tip

Setting up a simple monochromatic source can be easily done by passing in a list of numbers for energy and intensity. For example GVXR.Energy = [50,100,150] and GVXR.Intensity = [500,1000,200] will specify an X-ray source with 500, 1000, and 200 photons of 50,100 and 150 Mev respectively.

Action

Try changing the Beam energy from its current value of 0.08 Mev to 200 keV and observe what happens to the resulting image. you may also wish to try changing the beam from a cone beam to a parallel one.

8.3.2. Setting up the Detector:#

Setting up the detector we need to specify its position, shape and physical size.

Similar to the beam to set the position we use GVXR.Detect_PosX, GVXR.Detect_PosY and GVXR.Detect_PosZ again the default units are mm. However, you can easily change this to essentially any metric units by setting GVXR.Detect_Pos_units to the appropriate string (“mm”,”cm”,”m” etc …)[1]_.

For the number of pixels in each direction we use GVXR.Pix_X and GVXR.Pix_Y. Note: somewhat confusingly, up for the detector (i.e. Y) is along the Z axis in GVXR.

For the detector size we define the spacing between pixes with GVXR.Spacing_X and GVXR.Spacing_Y again the default units are mm but this can be changed with GVXR.Spacing_units.

8.3.3. Setting up the Sample:#

Next we need to set the properties of the Sample in this case our dragon model

For our sample we need specify four things:

  1. A 3D model of the object

  2. What the Sample is made from

  3. It’s position

  4. It’s size

  5. It’s orientation

First we need to specify the name of mesh file used. This is done with GVXR.mesh This can be any mesh format supported by the python package meshio. You only need to specify the filename including file extension.

To set the position, much like the X-Ray beam we use GVXR.Model_PosX, GVXR.Model_PosY and GVXR.Model_PosZ in this case these define the center of the cad mesh in 3D space.

However unlike the beam position these are optional and if they are not given the mesh we be centered on the scene at the origin (that is [0,0,0]).

For units you have two parameters:

  • GVXR.Model_Pos_units for the position

  • GVXR.Model_Mesh_units for the mesh itself

The default units are mm. However, once again you can easily change this to essentially any metric units by using the appropriate string (“mm”,”cm”,”m” etc …).

For scaling the mesh we have the optional values GVXR.Model_ScaleX, GVXR.Model_ScaleY and GVXR.Model_ScaleZ. These allow you to set a decimal scale factor in each dimension to reduce of increase the size of the model as needed. e.g. GVXR.Model_ScaleX=1.5 will scale the model size by 1.5 times in the X direction.

We can also optionally set the initial rotation with GVXR.rotation. This is set as a list of 3 floating point numbers to specify the rotation in degrees about the X,Y and Z axes. The default is [0,0,0] (i.e. no rotation). This is useful if the model is not correctly aligned initially.

A note about Rotation

If you have used GVXR previously you will know that rotation can be a pain to deal with because of how OpenGL defines rotations (heres a link to good article for those interested souls). Sufficed to say I personally find rotations very quickly become unintuitive especially when dealing with multiple rotations and translations in sequence.

As such in VirtualLab rotations (both initial rotation and for CT scans) are defined in the simplest way I can think off. They are clockwise, centered on the mesh, are fixed to the scene (world) axes and are performed in the order X then Y then Z. (i.e. a GVXR.rotation=[26.0,0,-15.3] will perform a sequence of 2 rotations first 26 degrees clockwise about the X axis, then 15.3 degrees anti-clockwise about the Z axis).

If that makes no sense to you don’t worry to much about it to much. If you are worried just leave it at the default [0,0,0] or play with the numbers until it looks right. Hopefully its intuitive enough.

Finally we need to set the material of the sample. For this we use three parameters:

  • GVXR.Material_list a list of materials used.

  • GVXR.Amounts a list of of relative amounts for each material, only used with mixtures.

  • GVXR.Density a list of the densities in g/cm^3 for each material.

These are all lists of values to define the properties for each material used.

To actually define materials we use GVXR.Material_list. Each item in the list defines the material. In our case for the sake of simplicity we only have one mesh so we only need one value.

Using multiple materials

The current example uses a single mesh made from a single material. The step up to multiple materials however, is slightly more complicated. We will be covering a multi-material example in the example 3.

However, due to limited development time/resources. In the current version of VirtualLab the use of multiple materials is only supported by using mesh regions in salome .med mesh files. We do hope to add multi-materials for all mesh formats via the use of multiple meshes in the near future. However, for now this is a known limitation of the current version.

In GVXR materials are split into three types: elements, mixtures (alloys) and Compounds. To define an element we supply the English name, symbol or atomic number. So for a single mesh made from Chromium we can use any of GVXR.Material_list = ['Chromium'], GVXR.Material_list = ['Cr'], or GVXR.Material_list = [24].

For a mixture we define a list of the elements in the mixture as atomic numbers (Note: names/symbols are not yet supported). You will also need to define the relative amounts of each using GVXR.Amounts with decimal values between 0.0 and 1.0 representing percentages from 0 to 100%. So for example a mixture of 25% Titanium and 75% Aluminum would be defined as: GVXR.Material_list = [[22,13]] and GVXR.Amounts = [[0.25,0.75]]

Compounds are defined as strings that represent the chemical formulae e.g. water would be 'H2O' whilst Aluminum Oxide would be 'Al2O3'. So for example a sample made from Silicon carbide would be defined as: GVXR.Material_list = ['SiC'].

For both Compounds and Mixtures you also will need to define the density for each material used, in g/cm^3. So for our previous example of Silicon carbide we can simply look up the density as 3.16 g/cm^3 thus we can use GVXR.Density=[3.16]

The density for the mixture of Titanium and Aluminum is more complex as there is no standard value so we need to approximate it. According to the royal society of chemistry Ti has a density of 4.506 g/cm^3 whilst Al is 2.70 g/cm^3. Thus for for our mixture using Vegard’s law we get a approximate density of

\[\rho_{Ti_{0.25}Al_{0.75}} \approx \rho_{Ti}*0.25 +\rho_{Al}*0.75 = (0.25*4.506)+(0.75*2.70) = 3.152 g/cm^3\]

Thus GVXR.Density=[3.152]

Task

The default material for this example is Aluminum. Try changing this to something much more dense like tungsten (hint the chemical symbol for tungsten is W whilst its atomic mass is 74) and observe what the effect is on the resulting image. You could also try changing the sample to Aluminum oxide (which for reference has a density of 3.987 g/cm^3).

8.4. Example 2: Defining scans using a Nikon .xect files.#

Many CT scanners use the Nikon .xect format to define scan parameters. These are just specially formatted text files ending in the .xect file extension. VirtualLab can read in parameters from these files.

To use these files you need to use GVXR.Nikon_file which sets the name of the nikon file you wish to use. This can either be in the Input directory or the absolute path to the file.

You will also at a minimum need to define

  • GVXR.Name

  • GVXR.Mesh

  • GVXR.Materail_list

As well as possibly amounts and density depending on what materials you have specified. All other parameters are either optional or will be taken from the equivalent parameters in the nikon file.

Action

As an example we will perform the same simulation as the previous example only this time we will define the setup with a nikon file.

This will require changing Parameters_Master to ‘TrainingParameters_GVXR_Nikon’

The RunFile RunTutorials.py should be setup as follows to run this simulation:

Simulation='Examples'
Project='Dragon'
Parameters_Master='TrainingParameters_GVXR_Nikon'
Parameters_Var=None

VirtualLab=VLSetup(
        Simulation,
        Project
        )

VirtualLab.Settings(
        Mode='Interactive',
        Launcher='Process',
        NbJobs=1
        )

VirtualLab.Parameters(
        Parameters_Master,
        Parameters_Var,
        RunCT_Scan=True
        RunCT_Recon=False,
        )

VirtualLab.CT_Scan()

Launch VirtualLab using the following command:

VirtualLab -f RunFiles/RunTutorials.py

The resulting xray image will be saved to Output/GVXR/Tutorials/GVXR_Images/Dragon_Nikon/Dragon_Nikon_1.tiff and will be identical to Fig. 8.2.

The following is a table of parameters in the nikon file and there equivalent parameters in VirtualLab.

Table 8.1 Parameters used from Nikon files#

Nikon Parameter

Notes

Equivalent Parameter

Units

Units for position of all objects

GVXR.Beam_Pos_units, GVXR.Det_Pos_units, GVXR.Model_Pos_units

Projections

Number of projections

GVXR.num_projections

AngularStep

Angular step between images in degrees.

GVXR.angular_step

DetectorPixelsX/Y

number of pixels along X/Y axis

GVXR.Pix_X/Pix_Y

DetectorPixelSizeX/Y

Size of pixels in X and Y

GVXR.Spacing_X/Y

SrcToObject

Distance in z from X-ray source to object, Note this is y in GVXR co-ordinates thus our beam position is defined as: [0,-SrcToObject,0]

GVXR.Beam_PosY

SrcToDetector

Distance in z from source to center of detector. Again this is equivalent to y in GVXR. Thus Detect_PosY is defined as: SrcToDetector-SrcToObject

GVXR.Detect_PosY

DetectorOffsetX/Y

detector offset from origin in X/Y

Detect_PosX/Z

XraykV

Tube voltage in kV

GVXR.Tube_Voltage

Filter_Material

Material used for beam filter

GVXR.Filter_Material

Filter_ThicknessMM

Thickness of beam filter in mm

GVXR.Filter_ThicknessMM

Please note however that a real nikon file will in general have a lot more parameters than these. As such any additional parameters defined in the file, along with comments in square brackets will simply be ignored.

Overriding values defined in a Nikon file.

You can define parameters in the input file that are also defined in the nikon file. If you do the parameters in the input file will override those in the nikon file.

8.5. Example 3: X-Ray CT-Scan with Multiple Materials#

In this example we will Simulate a X-ray CT scan using the AMAZE mesh that was previously used for the HIVE analysis in tutorial 3.

Note

If you haven’t completed tutorial 3 you will need to run the following command to generate the mesh

VirtualLab -f RunFiles/Tutorials/X-ray_imaging/Task3_mesh.py

Action

The RunFile RunTutorials.py should be setup as follows to run this simulation:

Simulation='HIVE'
Project='Tutorials'
Parameters_Master='TrainingParameters_GVXR'
Parameters_Var=None

VirtualLab=VLSetup(
        Simulation,
        Project
        )

VirtualLab.Settings(
        Mode='Interactive',
        Launcher='Process',
        NbJobs=1
        )

VirtualLab.Parameters(
        Parameters_Master,
        Parameters_Var,
        RunSim=False,
        RunCT_Scan=True,
        RunCT_Recon=False
        )

VirtualLab.CT_Scan()

A copy of this run file can be found in RunFiles/Tutorials/X-ray_imaging/Task3_Run.py

Looking at the file Input/HIVE/Tutorials/TrainingParameters_GVXR.py you will notice the Namespace GVXR has a few new options defined. Firstly, we are now using a more realistic beam spectrum instead of a monochromatic source. This is achieved by replacing GVXR.Energy with GVXR.Tube_Voltage. This tell VirtualLab to generate a beam spectrum from a simulated X-Ray Tube using xspecgen, in this case running at 440 KV. This is a more realistic X-Ray source than a simple monochromatic beam.

VirtualLab also has three other optional parameters related to X-Ray Tube spectrums which we are not used in this example.

  • GVXR.Tube_Angle common setting used by X-ray tubes default is 12.0

  • GVXR.Filter_Material material used for beam filter, used to remove certain frequencies

  • GVXR.Filter_ThicknessMM Thickness of beam filter

The second change to note here is we are now using a mesh with multiple materials. As mentioned earlier this is only currently implemented for salome med meshes using mesh regions. In our case the mesh has 3 regions Pipe, Block, and Tile.

For GVXR we have to define the corresponding materials using GVXR.Material_list in this case the pipe and block are both made from Copper. whilst the tile is made from the much denser Tungsten.

Action

Launch VirtualLab using the following command:

VirtualLab -f RunFiles/RunTutorials.py

The x-ray image generated for this sample can be found in Output/HIVE/Tutorials/GVXR-Images/AMAZE_single, and should look like Fig. 8.3.

https://gitlab.com/ibsim/media/-/raw/master/images/docs/screenshots/AMAZE_single_1.png

Fig. 8.3 X-Ray Image of the AMAZE component.#

8.6. Appendix#

Here is a complete list of all the available parameters that are used with GVXR alongside a brief explanation of there function. Note a default value of “-” indicates that this is a required parameter.

Table 8.2 Parameters in the GVXR Namespace#

Parameter

Notes

Default Value

Name

Name of the simulation

mesh

Name of mesh file used

Beam_PosX

Position of beam in X

2

Beam_PosY

Position of beam in Y

2

Beam_PosZ

Position of beam in Z

2

Beam_Pos_units

units for Beam position 1

mm

Beam_Type

Type of Source used, can be either point or parallel

point

Energy

Energy of Beam

0.0

Intensity

Number of Photons

0

Tube_Angle

Tube angle, if using spectrum calculation

12.0

Tube_Voltage

Tube Voltage, if using spectrum calculation

0.0

Filter_material

material for beam filter, optional parameter used in spectrum calculation.

None

Filter_ThicknessMM

Beam filter thickness in mm, optional parameter used in spectrum calculation.

None

energy_units

Units for Energy can be any of ‘eV’ ‘KeV’, ‘MeV’

Mev

Model_PosX

Position of center of the Cad Mesh in X

0.0 2

Model_PosY

Position of center of the Cad Mesh in Y

0.0 2

Model_PosZ

Position of center of the Cad Mesh in Z

0.0 2

Model_ScaleX

CAD Model scaling factor. Used to scale the model if needed.

1.0

Model_ScaleY

CAD Model scaling factor. Used to scale the model if needed.

1.0

Model_ScaleZ

CAD Model scaling factor. Used to scale the model if needed.

1.0

rotation

Initial rotation, in deg of Cad Model about X,Y and Z axis. Useful if the cad model is not aligned how you would like.

[0.0,0.0,0.0]

Model_Pos_units

units for Cad Mesh position 1

mm

Model_Mesh_units

units for Mesh itself 1

mm

Detect_PosX

Position of X-Ray detector in X

2

Detect_PosY

Position of X-Ray detector in Y

2

Detect_PosZ

Position of X-Ray detector in Z

2

Detect_Pos_units

units for X-Ray detector position 1

mm

Pix_X

Number of pixels for the X-Ray detector in X

2

Pix_Y

Number of pixels for the X-Ray detector in Y

2

SpacingX

distance between Pixels in X

0.5

SpacingY

distance between Pixels in Y

0.5

Spacing_units

units for Pixel spacing 1

mm

Material_list

list of materials used for each mesh or sub-mesh. See materials section for detailed usage.

Amounts

relative amounts of each material used. Note values used here must add up to 1.0

None

Density

density of each material used in g/cm^3.

None

num_projections

Number of projections generated for X-Ray CT Scan

1 2

angular_step

Angular step in deg to rotate mesh between each projection, Note: rotation is about the Y-axis in GVXR co-ordinates

0 2

use_tetra

Flag to tell GVXR you are using a volume mesh based on tetrahedrons. Not the default triangles. When this is set it tells GVXR to perform an extra step to extract just the mesh surface as triangle mesh. Note: whilst this is reasonably efficient it does add a small amount of overhead to the first run. However to mitigate this with multiple runs the new mesh is saved as ‘{filename}_triangles.{mesh_format}’ and is automatically re-used in future runs.

False

fill_percent

This setting, along with fill_value is used for removing ring artifacts during CT reconstruction. It allows you to fill a given percentage of the pixels from the 4 edges of the image (Top, bottom, left and right) with a specific value fill_value. If fill_value is not specified then the value used is calculated automatically from the average of the image background.

0.0

fill_value

value used to fill pixels at the image edges, when using fill_percent.

None

Nikon_file

Name of or path to a Nikon parameter .xtekct file to read parameters from, see section on Nikon file for more detailed explanation.

None

image_format

This option allows you to select the image format for the final output. If it is omitted (or set to None) the output defaults to a series of tiff images. However, when this option is set the code outputs each projection in any format supported by Pillow (see the PILLOW docs for the full list). Simply specify the image format you require as a string, e.g., GVXR.image_format='png'.

Tiff

bitrate

bitrate used for output images. Can be ‘int8’/’int16’ for 8 and 16 bit greyscale or ‘float32’ for raw intensity values.

float32

1(1,2,3,4,5)

Note for real space quantities units can be any off: “um”, “micrometre”, “micrometer”, “mm”, “millimetre”, “millimeter”, “cm”, “centimetre”, “centimeter”, “dm”, “decimetre”, “decimeter”, “m” “metre”, “meter”, “dam”, “decametre”, “decameter”, “hm”, “hectometre”, “hectometer”, “km”, “kilometre” “kilometer”

2(1,2,3,4,5,6,7,8,9,10,11,12,13)

These values are not required when using a Nikon .xect file as their corresponding values will be read in from that. If they are defined when using a nikon file they will override the corresponding value in the Nikon file. See section on Nikon files for more details.