Advanced Tutorial
Available modules
- Hounsfield Conversion
- SR Builder
- Index Tracker
- NISTParser
- TG43 Calculations
- TG263 Structures
- Dicom Reader
- Eggs Phantom Generator
Making SR (Structured Report)
Utility Functions
Making a SR
from pygrpm.dicom.sr import make_sr
sr_dicom = make_sr('<text_to_add>', '<optional_path_to_reference_dicom_file>', '<optional_purpose_of_reference_code>')
Storing a text into a DICOM SR
from pygrpm.dicom.sr import make_sr_from_text
# Make SR DICOM dataset from text
sr_with_text = make_sr_from_text("My Text", ['./CT1.dcm', './CT2.dcm', './CT3.dcm', ...])
# Afterward, can be written to a file like this:
sr_with_text.save_as('./my_sr.dcm')
SR Builder
Generate DICOM SR File
from pygrpm.dicom.sr import SRBuilder
sr_builder = SRBuilder('<sr_target_file_path>', '<purpose_of_reference_code>')
sr_builder.build()
sr_builder.dicom_sr #How to access the Dicom SR object
- A DicomSRBuilder object is created with parameters:
- sr_target_file_path: Path of the reference DICOM file to use to populate the DICOM SR. Dicom files are also accepted directly
-
purpose_of_reference_code: Dictionary with information tags on WHY the target file is used to generate the DICOM SR For example:
python { "CodeValue": "SR", "CodeMeaning": "Structured Report Document", "CodingSchemeDesignator": "DCM", }
For more information on what this dictionary might contain: https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/sect_8.8.html#table_8.8-1a and https://dicom.nema.org/medical/dicom/2018b/output/chtml/part03/chapter_8.html
The most common CodingSchemeDesignator is "DCM", here is a reference to the values accepted for this coding scheme: https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_D.html
-
Calling the build() method then builds the DICOM SR, assembling MetaData with available data
Adding a content sequence
sr_dicom.add_content_sequence(<content_sequence>)
- Adds the content sequence given as parameter to the DICOMSR object.
A content sequence is a structured content (a list generally) giving relevant information on the associated document.
Includes, minimally:
- The Value Type (TEXT, NUM, CODE, DATETIME, etc.)
- The Value (must match in type with the Value Type)
- The ConceptNameCodeSequence in the same format as the purpose_of_reference_code used above
For example:
python
{
"ValueType": "TEXT",
"Value": "This is some text",
"ConceptNameCodeSequence": {
"CodeValue": "1233",
"CodeMeaning": "Test",
"CodingSchemeDesignator": "DCM",
},
"SomeOtherTag" : "Some additionnal information"
}
- The content sequence is validated to appropriate standards before adding it (see https://dicom.nema.org/medical/dicom/2020b/output/chtml/part03/sect_C.17.3.2.html for more information)
Containers
Container objects are common content sequences that must respect additionnal format restrictions. Here is an example of a valid container object for the method add_content_sequence:
{
"ValueType": "CONTAINER",
"ConceptNameCodeSequence": {
"CodeValue": "DOC",
"CodeMeaning": "Document",
"CodingSchemeDesignator": "DCM",
},
"ContinuityOfContent": "SEPERATE",
"Value": [
{
"RelationshipType": "HAS PROPERTIES",
"ValueType": "TEXT",
"ConceptNameCodeSequence": {
"CodeValue": "113012",
"CodeMeaning": "Key Object Description",
"CodingSchemeDesignator": "DCM",
},
"Value": "This is a text",
}
],
}
- Refer to https://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.17.3.2.4.html for more information
Additionnal Information
Users who wish to have more information on the creation of SR are invited to read documentation concerning SR dose content sequences and DICOM part 03 section 17.
Acknowledgements
- This work is based on a previous project from Samuel Ouellet (https://gitlab.chudequebec.ca/sam23/dicom-sr-builder)
- DICOM SR files assembled by this module are in accordance with PS3.6 2023b standards. PS3.6 standard can (and most certainly will) change over time. See https://dicom.nema.org/medical/dicom/current/output/html/part06.html for current standards.
- Thanks to Gabriel Couture for some much needed revision work
Contact
Pierre-Luc Asselin, B. Ing., plass2@ulaval.ca
TG43
Python package to calculate the TG43 formalism based on xarray and the data from ESTRO.
Available seeds
- SelectSeed
- MicroSelectronV2
- MBDCA
- Flexisource
Usage
import matplotlib.pyplot as plt
from pygrpm.tg43 import Seed
seed = Seed("MBDCA")
print(seed.gL)
print(seed.F)
# Display dosimetric grid
grid = seed.grid()
# Plot arbitrary 100th slice
plt.imshow(grid.data[:, :, 100])
plt.show()
# Plot mean dose vs radius with std
mean = seed.get_mean(grid)
std = seed.get_std(grid)
plt.errorbar(mean["r"].data, mean.data, std.data, fmt='o')
plt.show()
Index tracker
Python package to allow scrolling through slices of 3-D images using the matplotlib backend.
Usage
Here is shortly described the main usage of this module. See the docstring of each function, class, method and attribute for more details.
In the first example shown below, a 3-D array is populated with random values,
and the object tracker
is initialized with a few arguments. The first one represents the
unpacked tuple of the objects (fig,ax)
, followed by the 3-D array that represents the image,
and other parameters to customize how the image is shown by the matplotlib backend. It is
important to provide slice_axis
, in order to define where the slices are located in the
image. The object contains as members ax
, im
, and cb
(colorbar), which allows further
customization:
# 3-D array with shape (512,512,10)
random_array = np.random.rand(512,512,10)
tracker = IndexTracker(*plt.subplots() , # plt objects
random_array , # 3-D array of image
vmin = 0 , # Minimum value (colormap)
vmax = 0.8 , # Maximum value (colormap)
slice_axis = 2 , # Axis containing slices
cmap='gray') # Colormap
tracker.ax.set_title('My 3-D random image')
tracker.show()
In the following example, it is shown how easily one can format the image style, either
before or after the object tracker
is initialized:
# Formatting the plot
random_array = np.random.rand(10,512,512)
fig, ax = plt.subplots()
fig.set_tight_layout(True)
ax.set_title('My first 3-D random image')
tracker = IndexTracker(fig, ax, random_array, slice_axis=0)
# Change min and maximum value of colormap
minv = 0.0
maxv = 0.03
tracker.im.set_clim(minv, maxv)
# Change colorbar configuration
tracker.colorbar(extend='both')
tracker.show()
# Passing a color normalization
from matplotlib.colors import LogNorm
random_array = np.random.rand(10,512,512)
tracker = IndexTracker(*plt.subplots(),
random_array,
slice_axis=0,
cmap='viridis',
norm=LogNorm(vmin=0.01, vmax=1))
tracker.show()
Jupyter environment
In order to use IndexTracker
inside the jupyter environment
(e.g. Jupyter lab, and Jupyter Notebook*), it is necessary
to add the following statement prior to your code:
import matplotlib
matplotlib.use('QtAgg')
tracker = IndexTracker(...)
This statement changes the matplotlib's backend, allowing to show images in a new window.
See this for more information
NISTParser
A simple class to extract information from certain NIST webpages. At the time of writing this covers atomic and electronic cross-sections, material attenuation, as well as material composition.
get_cross_sections
This method retrieves the desired cross-sections of an element at given energies on the NIST website in (barns/electron), barn=10^-24cm^2.
Simple use example:
import numpy as np
from pygrpm.material.nistparser import get_cross_sections
# Define the energies in KeV
# Numpy array is not mandatory, can be any sequence
energies = np.linspace(30, 200, 200)
# Prints the returned list
print(get_cross_sections("H", energies))
get_electronic_cross_section
See get_cross_section(), method use is identical minus the options argument
get_attenuations
Method to retrieve the attenuation of a material in cm^2/g at given energies on the NIST website.
* Note the option
parameter which can specify the attenuation physics types
* Note the outopt
parameter which can alter the returned information
Example is similar to get_cross_sections()
get_composition
This method is used to get and parse material composition from https://physics.nist.gov/cgi-bin/Star/compos.pl
Simple use example:
from pygrpm.material.nistparser import get_composition, NISTMaterials
# Prints the returned dictionary
print(get_composition(NISTMaterials.M3_WAX))
Note that get_composition expects the material to be of instance NISTMaterials
Acknowledgements
- This submodule makes use of the
HTMLTablePasrer
built by Josua Schmid, further information can be found in thepygrpm.nistparser.nistparser.py
file header. - This submodule is also dependent on the data provided by https://www.nist.gov/pml
Hounsfield conversion
A helper class meant to offer a quick and simple means to convert an HU array to density values based on a provided conversion table. Note that the conversion factors for HU to density are generally provided by the CT manufacturer. This class is currently only able to be read under csv format type.
General notes
The conversion factors for HU to density are generally provided by the CT manufacturer.
This file is currently only able to be read under csv
format type.
Usage
Using the following sample data as ./curve.csv
file,
HU,Mass density [g/cm^3]
-1000,0.00121
-804,0.217
-505,0.508
-72,0.967
-32,0.99
7,1.018
44,1.061
52,1.071
254,1.159
4000,3.21
A call through the class can be made to rescale an arbitrary array of Hounsfield unit to density values.
import numpy as np
from pygrpm.material.hounsfield_conversion import ConvertHUToDensity
fithu = ConvertHUToDensity()
my_curve = fithu.load_curve_csv("./curve.csv")
# Note that setting plot to True generates a matplotlib plot of the curve fitting
data, fit_params, labels = fithu.fit_curve(my_curve, plot=True)
# Fit returns unused in this example
my_real_image = np.array([-980, -1000., -823., 1, 20, 700, 2900])
densities = fithu.apply_fit(my_real_image, my_curve)
print(densities) # [0.02702014 0.00652871 0.18787786 1.0291972 1.03955733 1.41034076, 2.60993423]
Note that the class will automatically "wrap" any HU values beyond its set extrema to their nearest allowed extrema value. These values can be altered using the .set_extrema(min_val, max_val)
method.
TG263
Validating the structure name
from pygrpm.tg263.nomenclature import is_structure_name_allowed
result = is_structure_name_allowed('Prostate')
# Result is `True`
This will simply specify if the parameter string is an allowed structure Primary Name in the TG263 structure database.
Finding a structure
from pygrpm.tg263.nomenclature import find_structures
result = find_structures('SpinalCord')
This will print all information on every structure that as a TG263 primary name matching the parameter string. Structure information is return in dictionary format, and all structures are returned into a list.
Printing structure info
from pygrpm.tg263.nomenclature import print_structure_info
print_structure_info('<structure_to_print>')
This will print relevant structure information on the structure given as a parameter.
Validating a structure's format
from pygrpm.tg263.nomenclature import is_structure_valid
validation, log = is_structure_valid('<structure_to_validate>')
This will return a boolean (validation) wheiter the structure is valid. If not, the log will explain more precisely the nature of the problem.
Criterias are as follows:
- Structures are python libraries
- Structures must contain keys* for:
- tg263PrimaryName
- tg263ReverseOrderName
- targetType
- majorCategory
- minorCategory
- anatomicGroup
- description
- fmaid
- nCharacters
- Every key as a string value, except for fmaid and nCharacters, which must be integers.
-
String values cannot contain any of these characters: "<>:"'/|?*()#$"
-
The containt of requisite keys is detailled in the RPT_263 report (link below).
DicomReader
A helper class meant to ingest a given folder of Dicom files and sort them into studies and series. This is aided with the DicomSeries and DicomStudy dataclasses.
Basic Usage
The class constructor is to be fed a system path for a folder containing, under any folder structure, the desired Dicom files.
from pygrpm.dicom import DicomReader
my_path = "/path/to/dicoms/folder/"
reader = DicomReader(my_path)
reader.studies # list of DicomStudy classes
reader.studies[0].series # list of DicomSeries classes
Dicoms can also be fed through unsorted through a generator with the following
from pygrpm.dicom import DicomReader
my_path = "/path/to/dicoms/folder/"
reader = DicomReader(my_path)
for study in reader.yield_dicoms():
do_something(study)
DicomReader
There are a few helper methods available. Notably <reader>.get_study(<study_UID>)
and <reader>.get_series(<study_UID>, <series_UID>)
for easy access to given series or studies.
DicomStudy
It is also possible to filter a study by present modalities, <study>.filter_by_modality("CT")
which would return a list of DicomSeries dataclasses of modality "CT"
.
DicomSeries
The DicomSeries dataclass offers easy access to the first instance of the series through <series>.first_instance
as sorted by Z-slice position. Custom sorting is also available through <series>.sort_instances_by_tag(<dicom_tag>)
.
<series>.numpy
returns a constructed numpy array from multiple 2D PixelData components as in the case of CT images, or fast-forwards pydicom's pixel_array in the case of 3D arrays such as in RTDose or RTStruct.
The pydicom FileDatasets are available within their respective DicomSeries dataclass within the <series>.instances
list.
Egsphant Generator
A class designed to transform data parsed from a CT volume in numpy format, and provide an egsphant type text file.
Usage
The following example will assume that the user has already determined volume center, pixel spacing, and stacked the CT volume in a 3D numpy array. Note that the volume is expected to be in HU values for proper material assignment.
import numpy as np
from pygrpm.geometry.egsphant import make_egsphant_from_numpy
spacing = np.array(...)
center = np.array(...)
volume = np.array(...) # Assuming slices in last position
# Select built-in materials list CT01 and specify slices in last axis
myegs = make_egsphant_from_numpy(volume, spacings=spacing, center=center,
materials="CT01", slice_axis=-1)
# Can see the full string contents directly if desired
contents = myegs.content
myegs.write_to_file("./path/to/myvolume.egsphant")
Individual sections of the egsphant can also be accessed via the header
, voxel_position_string
, material_string
, and density_string
properties of the provided Egsphant class.
Acknowledgements
This work is a basic implementation of the TG263 (https://www.aapm.org/pubs/reports/RPT_263.pdf)
The allowed structure names (and corresponding information) were taken from https://www.aapm.org/pubs/reports/RPT_263_Supplemental/ .
The initial allowed structure names were taken from the ESAPIX project (https://github.com/rexcardan/ESAPIX), made by Rex Cardan. The ESAPIX license is included in the LICENSE file.