Module ctsimu.scenario
Read, write, handle and manipulate a CTSimU scenario.
Reading and writing scenario files
To read a scenario file, you can pass it to the constructor of
a Scenario
object:
# -*- coding: UTF-8 -*-
# File: examples/scenario/01_read_scenario.py
from ctsimu.scenario import Scenario
s = Scenario("example.json")
You can also employ the function Scenario.read()
:
from ctsimu.scenario import Scenario
s = Scenario()
s.read("example.json")
To write a scenario file, use the Scenario.write()
function.
from ctsimu.scenario import Scenario
s = Scenario()
s.read("example.json")
s.write("example_new.json")
Scenarios are always written in the currently supported file format version
of the CTSimU scenario file format. Any additional information from
the originally imported scenario file that was stored in non-standard
parameters, such as comments, is lost. However, the "simulation"
section
of the scenario file, which stores proprietary, simulation-software specific
parameters, is completely preserved.
Manipulating scenarios
Parameter getters and setters
A Scenario
object provides the same sub-object
structure as the JSON structure of a CTSimU scenario.
In the toolbox, each parameter of the scenario is an object of the class
Parameter
. It provides a function
Parameter.get()
to get its current value and a
function Parameter.set()
to set its standard and
current value.
The current value is the value that the parameter has in the frame that is currently set (see below). Because parameter values can drift throughout a CT scan, the current value of a parameter depends on the current frame number. The standard value is the parameter value without any drifts considered.
In the following example, we get the value of the environment temperature and then set it to a new value.
# -*- coding: UTF-8 -*-
# File: examples/scenario/02_getters_and_setters.py
from ctsimu.scenario import Scenario
s = Scenario("example.json")
# Get current temperature:
T = s.environment.temperature.get()
print(f"Currently, it's {T}°C in the room.")
# Currently, it's 20°C in the room.
# Set new temperature to 23°C:
s.environment.temperature.set(23)
# Get new temperature:
T = s.environment.temperature.get()
print(f"Now, it's {T}°C in the room.")
# Currently, it's 23°C in the room.
# Native unit and preferred unit:
print(s.environment.temperature.native_unit)
print(s.environment.temperature.preferred_unit)
Units
Within the toolbox, parameter values are always in their native units, which means the getter functions return values in native units and the setter functions expect a value in the parameter's native unit. The following table gives an overview of the native units used in the toolbox.
Quantity | Native unit |
---|---|
dimensionless | None |
Length | "mm" |
Angle | "rad" by default, |
"deg" for start_angle and stop_angle of stage |
|
Time | "s" |
Current | "mA |
Voltage | "kV |
Mass density | "g/cm^3" |
Spatial frequency | "lp/mm" |
Angular velocity | "deg/s" |
Temperature | "C" |
Boolean | "bool" |
String | "string" |
There are also further dummy units which the toolbox accepts in scenario
descriptions, but handles them like no unit at all (dimensionless):
"px"
, "1/J"
, "relative"
.
Additionally, each parameter has a preferred unit. This is the parameter's unit used in the JSON scenario file. When writing a scenario file, the value is converted from the internal native unit to the preferred unit.
Frames
Setting the frame
A scenario defines a CT trajectory, typically a rotation of the sample stage. While the motion takes place, projection images are taken: each projection image represents a scenario frame with its specific CT geometry.
To set the frame number for the scenario, use the function
Scenario.set_frame()
. The function accepts the frame number
(starting at zero) and as second argument a Boolean value that decides if the
geometry should be set up as seen by the reconstruction software (True
):
any drifts and deviations that are not known_to_reconstruction
will be ignored
in this case.
# -*- coding: UTF-8 -*-
# File: examples/scenario/03_set_frame.py
from ctsimu.scenario import Scenario
s = Scenario("example.json")
s.set_frame(frame=10, reconstruction=False)
geo = s.current_geometry()
print("Stage coordinate system for frame 10:")
print(geo.stage)
"""
Stage coordinate system for frame 10:
Center: [275. 0. 0.]
u: [-0.93896467 0.34045905 -0.04932528]
v: [-0.34274809 -0.93813153 0.04932528]
w: [-0.02948036 0.06322084 0.99756405]
"""
Geometry of the current frame
After the scenario frame is set, the current Geometry
can
be accessed through the function Scenario.current_geometry()
.
This can be helpful if you want to calculate projection matrices for each frame
or use the coordinate systems of the individual parts for something else.
The function Scenario.n_frames()
returns
the number of frames defined in the scenario.
The following example shows how to iterate through the frames of a scenario, calculate a projection matrix for each frame, and access the coordinate systems of the scenario's parts (X-ray source, stage, detector and samples).
It also shows how to transform the sample's coordinate system into world
coordinates. Sample coordinates are given in terms of the stage coordinate
system if the sample is attached to the stage (the usual behaviour). Here,
the method CoordinateSystem.change_reference_frame()
from
the geometry sub-module is used to transform the sample coordinate system.
# -*- coding: UTF-8 -*-
# File: examples/scenario/04_frame_geometry.py
from ctsimu.scenario import Scenario
import ctsimu.geometry # for ctsimu_world
s = Scenario("example.json")
for frame_number in range(s.n_frames()):
# Set frame number:
s.set_frame(frame=frame_number, reconstruction=False)
# Calculate projection matrix:
geo = s.current_geometry()
pmatrix = geo.projection_matrix(mode="OpenCT")
# Individual coordinate systems for this frame:
cs_source = s.source.coordinate_system
cs_stage = s.stage.coordinate_system
cs_detector = s.detector.coordinate_system
# First sample in stage coordinate system:
sample = s.samples[0] # get first sample
cs_sample = sample.coordinate_system.get_copy()
# Transform sample coordinate system from
# stage to world coordinates if it is
# attached to the stage:
if sample.is_attached_to_stage():
cs_sample.change_reference_frame(
cs_from=cs_stage,
cs_to=ctsimu.geometry.ctsimu_world
)
print(f"Frame {frame_number}")
print("================")
print("Stage coordinates in world:")
print(cs_stage)
print(f"Sample coordinates in world:")
print(cs_sample)
Reconstruction config files
OpenCT & CERA
The toolbox can generate reconstruction configuration files for SIEMENS CERA
and in the OpenCT format (used by VGSTUDIO) directly from a scenario file.
The simplest way is to use the functions
Scenario.write_CERA_config()
and
Scenario.write_OpenCT_config()
:
# -*- coding: UTF-8 -*-
# File: examples/scenario/05_reconstruction_configs.py
from ctsimu.scenario import Scenario
s = Scenario("example.json")
s.write_CERA_config(
save_dir="cera_recon",
basename="example",
create_vgi=True
)
s.write_OpenCT_config(
save_dir="openct_recon",
basename="example",
create_vgi=True,
variant="free",
abspaths=True
)
For a detailed explanation of the parameters, please see the documentation of the two functions.
Projection and volume parameters
Reconstructions need to read in projection images and will create a tomogram volume as output. In the example above, information about the projection images and tomogram volume is generated automatically. For example, the number of voxels in the tomogram and the voxel size are calculated automatically from the magnification and detector geometry.
If you want to change these parameters, you must change the scenario's metadata before you create the config files. If you have a metadata file that describes the projections and tomogram volume, you can import this file directly after reading the scenario:
from ctsimu.scenario import Scenario
s = Scenario("example.json")
s.read_metadata("recon_metadata.json")
It is also possible to set up the scenario's metadata manually by following the same structure as defined in the metadata file. In the following example, the projection image files are assumed to be sequentially numbered (four digits), starting at zero.
# -*- coding: UTF-8 -*-
# File: examples/scenario/06_reconstruction_metadata
from ctsimu.scenario import Scenario
s = Scenario("example.json")
# Information about RAW projection images:
s.metadata.output.projections.filename.set("example_%04d.raw")
s.metadata.output.projections.datatype.set("float32")
s.metadata.output.projections.byteorder.set("little")
s.metadata.output.projections.headersize.file.set(1024)
# ...or just TIFF files:
s.metadata.output.projections.filename.set("example_%04d.tif")
# Projection image size:
s.metadata.output.projections.dimensions.x.set(1500) # pixels
s.metadata.output.projections.dimensions.y.set(1000) # pixels
s.metadata.output.projections.pixelsize.x.set(0.1) # mm
s.metadata.output.projections.pixelsize.y.set(0.1) # mm
# Maximum free-beam intensity gray value:
s.metadata.output.projections.max_intensity.set(44000)
# No flat and dark field images; let's assume
# projections are already corrected:
s.metadata.output.projections.dark_field.filename.set(None)
s.metadata.output.projections.flat_field.filename.set(None)
# Information about the tomogram:
s.metadata.output.tomogram.filename.set("example_recon.raw")
s.metadata.output.tomogram.datatype.set("uint16")
s.metadata.output.tomogram.byteorder.set("little")
# Tomogram size:
s.metadata.output.tomogram.dimensions.x.set(1500) # voxels
s.metadata.output.tomogram.dimensions.y.set(1500) # voxels
s.metadata.output.tomogram.dimensions.z.set(1000) # voxels
s.metadata.output.tomogram.voxelsize.x.set(0.05) # mm
s.metadata.output.tomogram.voxelsize.y.set(0.05) # mm
s.metadata.output.tomogram.voxelsize.z.set(0.05) # mm
s.write_CERA_config(
save_dir="cera_recon",
basename="example",
create_vgi=True
)
s.write_OpenCT_config(
save_dir="openct_recon",
basename="example",
create_vgi=True,
variant="free",
abspaths=True
)
Expand source code
# -*- coding: UTF-8 -*-
"""Read, write, handle and manipulate a CTSimU scenario.
.. include:: ./documentation.md
"""
import math
import os
from datetime import datetime
from ..helpers import *
from ..geometry import *
from . import *
from .detector import Detector
from .source import Source
from .stage import Stage
from .sample import Sample
from .file import File
from .environment import Environment
from .acquisition import Acquisition
from .material import Material
from .metadata import Metadata
class Scenario:
"""Representation of a CTSimU scenario. Reads, writes, manages
and manipulates scenarios.
Attributes
----------
detector : ctsimu.scenario.detector.Detector
Detector object of the scenario.
source : ctsimu.scenario.source.Source
X-ray source object of the scenario.
stage : ctsimu.scenario.stage.Stage
Sample stage object of the scenario.
samples : list
List of samples in the scenario, each an object of class
`ctsimu.scenario.sample.Sample`.
file : ctsimu.scenario.file.File
Information about the scenario file.
environment : ctsimu.scenario.environment.Environment
Environment properties of the scenario.
acquisition : ctsimu.scenario.acquisition.Acquisition
CT acquisition parameters.
materials : list
List of materials used in the scenario, each an object of
class `ctsimu.scenario.material.Material`.
simulation : dict
Dictionary of simulation software-specific properties
from the scenario file.
metadata : ctsimu.scenario.metadata.Metadata
Metadata information about projection images and
the reconstruction volume. This information depends on the output
of the CT scanner or simulation software, not on the scenario
itself.
"""
def __init__(self, filename:str=None, json_dict:dict=None):
"""The scenario can optionally constructed by passing the path to
a scenario file or by passing a Python dictionary that contains
scenario information:
Parameters
----------
filename : str, optional
Path to a [CTSimU scenario] file.
[CTSimU scenario]: https://bamresearch.github.io/ctsimu-scenarios
json_dict : dict, optional
A Python dictionary containing a CTSimU scenario.
The dictionary must obey the same structure as a
CTSimU JSON scenario file.
"""
self.detector = Detector(_root=self)
self.source = Source(_root=self)
self.stage = Stage(_root=self)
self.samples = list()
self.file = File(_root=self)
self.environment = Environment(_root=self)
self.acquisition = Acquisition(_root=self)
self.materials = list()
self.simulation = None # simply imported as dict
# CTSimU metadata structure:
# Necessary for generation of reconstruction configs.
self.metadata = Metadata(_root=self)
# Subgroups are necessary for the 'get' and 'set' command:
self.subgroups = [
self.detector,
self.source,
self.stage,
self.file,
self.environment,
self.acquisition,
self.metadata
]
self.current_frame = 0
self.current_scenario_path = None
self.current_scenario_file = None
self.current_scenario_basename = None
self.current_scenario_directory = None
self.current_metadata_path = None
self.current_metadata_file = None
self.current_metadata_basename = None
self.current_metadata_directory = None
self.metadata_is_set = False
if filename is not None:
self.read(filename=filename)
elif json_dict is not None:
self.read(json_dict=json_dict)
def read(self, filename:str=None, json_dict:dict=None):
"""Import a CTSimU scenario from a file or a given
scenario dictionary.
Parameters
----------
filename : str
Path to a CTSimU scenario file.
Default value: `None`
json_dict : dict
Provide a dictionary with a scenario structure instead of a file.
Default value: `None`
"""
self.current_scenario_path = None
self.reset()
if filename is not None:
json_dict = read_json_file(filename=filename)
self.current_scenario_path = filename
self.current_scenario_directory = os.path.dirname(filename)
self.current_scenario_file = os.path.basename(filename)
self.current_scenario_basename, extension = os.path.splitext(self.current_scenario_file)
elif not isinstance(json_dict, dict):
raise Exception("Scenario: read() function expects either a filename as a string or a CTSimU JSON dictionary as a Python dict.")
return False
# If a file is read, we want to make sure that it is a valid
# and supported scenario file:
if isinstance(json_dict, dict):
file_type = get_value(json_dict, ["file", "file_type"])
if file_type != "CTSimU Scenario":
raise Exception(f"Invalid scenario structure: the string 'CTSimU Scenario' was not found in 'file.file_type' in the scenario file {file}.")
file_format_version = get_value(json_dict, ["file", "file_format_version"])
if not is_version_supported(ctsimu_supported_scenario_version, file_format_version):
raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.")
else:
raise Exception(f"Error when reading the scenario file: {filename}. read_json_file did not return a Python dictionary.")
self.detector.set_from_json(json_dict)
self.source.set_from_json(json_dict)
self.stage.set_from_json(json_dict)
json_samples = json_extract(json_dict, ["samples"])
if json_samples is not None:
for json_sample in json_samples:
s = Sample(_root=self)
s.set_from_json(json_sample, self.stage.coordinate_system)
self.samples.append(s)
self.file.set_from_json(json_extract(json_dict, [self.file._name]))
self.environment.set_from_json(json_extract(json_dict, [self.environment._name]))
self.acquisition.set_from_json(json_extract(json_dict, [self.acquisition._name]))
self.simulation = json_extract(json_dict, ["simulation"])
json_materials = json_extract(json_dict, ["materials"])
for json_material in json_materials:
m = Material(_root=self)
m.set_from_json(json_material)
self.materials.append(m)
self.set_frame(0, reconstruction=False)
if not self.metadata_is_set:
self.create_default_metadata()
def reset(self):
"""Reset scenario: delete all drifts, deviations and
sample and material information."""
for group in self.subgroups:
group.reset()
def reset_metadata(self):
"""Reset scenario's metadata information."""
self.current_metadata_path = None
self.current_metadata_file = None
self.current_metadata_basename = None
self.current_metadata_directory = None
self.metadata_is_set = False
# Create new, empty metadata:
self.metadata = Metadata()
def read_metadata(self, filename:str=None, json_dict:dict=None, import_referenced_scenario:bool=False):
"""Import metadata from a CTSimU metadata file or a given
metadata dictionary.
Parameters
----------
filename : str
Path to a CTSimU metadata file.
Default value: `None`
json_dict : dict
Provide a dictionary with a metadata structure instead of a file.
Default value: `None`
import_referenced_scenario : bool
Import the scenario JSON file that's referenced in the metadata file?
Generates a warning if this fails.
The scenario definition will be searched at two locations in the following order:
1. Try to read from external file defined by `acquisition_geometry.path_to_CTSimU_JSON`.
2. Try to read embedded scenario definition from
`simulation.ctsimu_scenario`. Note that external drift files
specified in the scenario will likely fail to load. An error
will be issued in this case.
Default value: `False`
"""
if filename is not None:
json_dict = read_json_file(filename=filename)
self.current_metadata_path = filename
self.current_metadata_directory = os.path.dirname(filename)
self.current_metadata_file = os.path.basename(filename)
self.current_metadata_basename, extension = os.path.splitext(self.current_metadata_file)
# If a file is read, we want to make sure that it is a valid
# and supported metadata file:
if isinstance(json_dict, dict):
file_type = get_value(json_dict, ["file", "file_type"])
if file_type != "CTSimU Metadata":
raise Exception(f"Invalid metadata structure: the string 'CTSimU Metadata' was not found in 'file.file_type' in the metadata file {file}.")
fileformatversion = get_value(json_dict, ["file", "file_format_version"])
if not is_version_supported(ctsimu_supported_metadata_version, fileformatversion):
raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.")
else:
raise Exception(f"Error when reading the metadata file: {filename}. read_json_file did not return a Python dictionary.")
# If we get a `json_dict` as function parameter, we do not
# test for a valid version because reduced/simplified metadata
# structures should be supported as well.
self.metadata.set_from_json(json_dict)
self.metadata_is_set = True
if import_referenced_scenario:
# Import the scenario that's referenced in the metadata file.
ref_file = self.metadata.get(["acquisition_geometry", "path_to_CTSimU_JSON"])
import_success = False
try:
if (ref_file is not None) and (ref_file != ""):
if isinstance(ref_file, str):
ref_file = abspath_of_referenced_file(self.current_metadata_path, ref_file)
else:
raise Exception("read_metadata: path_to_CTSimU_JSON is not a string.")
# Try to import scenario:
self.read(filename=ref_file)
import_success = True
except Exception as e:
warnings.warn(str(e))
import_success = False
if not import_success:
# Try to import the embedded scenario structure.
if json_exists_and_not_null(json_dict, ["simulation", "ctsimu_scenario"]):
self.read(json_dict=json_dict["simulation"]["ctsimu_scenario"])
import_success = True
# Create default metadata in case the original
# metadata file did not contain all information that's needed:
self.create_default_metadata()
# Re-import metadata:
self.metadata.set_from_json(json_dict)
def create_default_metadata(self):
"""Set default metadata from scenario information,
to use if no metadata file is available."""
self.reset_metadata()
geo = self.current_geometry()
cera_parameters = geo.get_CERA_standard_circular_parameters()
# Basename:
if self.current_scenario_basename is not None:
self.current_metadata_basename = self.current_scenario_basename
basename = self.current_metadata_basename
n_projections = self.acquisition.get("number_of_projections")
projection_filename = f"{basename}_{counter_format(n_projections)}.raw"
# Prepare filename for dark fields:
n_darks = self.acquisition.dark_field.get("number")
dark_filename = None
if n_darks is not None:
if n_darks > 0:
if n_darks > 1:
dark_filename = f"{basename}_dark_{counter_format(n_darks)}.raw"
else:
dark_filename = f"{basename}_dark.raw"
# Prepare filename for flat fields:
n_flats = self.acquisition.flat_field.get("number")
flat_filename = None
if n_flats is not None:
if n_flats > 0:
if n_flats > 1:
flat_filename = f"{basename}_flat_{counter_format(n_flats)}.raw"
else:
flat_filename = f"{basename}_flat.raw"
n_cols = self.detector.get("columns")
n_rows = self.detector.get("rows")
pixel_size_u = self.detector.pixel_pitch.get("u")
pixel_size_v = self.detector.pixel_pitch.get("v")
voxels_x = n_cols
voxels_y = n_cols
voxels_z = n_rows
voxelsize_x = cera_parameters["voxelsize"]["x"]
voxelsize_y = cera_parameters["voxelsize"]["y"]
voxelsize_z = cera_parameters["voxelsize"]["z"]
now = datetime.now()
metadata = {
"file": {
"name": basename,
"description": self.file.get("description"),
"contact": self.file.get("contact"),
"date_created": now.strftime("%Y-%m-%d"),
"date_changed": now.strftime("%Y-%m-%d"),
"file_type": "CTSimU Metadata",
"file_format_version": {
"major": ctsimu_supported_metadata_version["major"],
"minor": ctsimu_supported_metadata_version["minor"]
}
},
"output": {
"system": None,
"date_measured": None,
"projections": {
"filename": projection_filename,
"number": n_projections,
"frame_average": self.acquisition.get("frame_average"),
"max_intensity": self.detector.get(["gray_value", "imax"]),
"datatype": "uint16",
"byteorder": "little",
"headersize": {
"file": 0,
"image": 0
},
"dimensions": {
"x": {"value": n_cols, "unit": "px"},
"y": {"value": n_rows, "unit": "px"}
},
"pixelsize": {
"x": {"value": pixel_size_u, "unit": "mm"},
"y": {"value": pixel_size_v, "unit": "mm"}
},
"dark_field": {
"number": n_darks,
"frame_average": self.acquisition.dark_field.get("frame_average"),
"filename": dark_filename,
"projections_corrected": False
},
"flat_field": {
"number": n_flats,
"frame_average": self.acquisition.flat_field.get("frame_average"),
"filename": flat_filename,
"projections_corrected": False
},
"bad_pixel_map": {
"filename": None,
"projections_corrected": False
}
},
"tomogram":
{
"filename": f"{basename}_recon.raw",
"datatype": "float32",
"byteorder": "little",
"headersize": {
"file": 0,
"image": 0
},
"dimensions": {
"x": {"value": voxels_x, "unit": "px"},
"y": {"value": voxels_y, "unit": "px"},
"z": {"value": voxels_z, "unit": "px"}
},
"voxelsize": {
"x": {"value": voxelsize_x, "unit": "mm"},
"y": {"value": voxelsize_y, "unit": "mm"},
"z": {"value": voxelsize_z, "unit": "mm"}
}
}
},
"acquisition_geometry": {
"path_to_CTSimU_JSON": self.current_scenario_path
},
"reconstruction": {
"software": None,
"settings": { }
},
"simulation": {
"ctsimu_scenario": None
}
}
self.read_metadata(json_dict=metadata, import_referenced_scenario=False)
def write(self, filename:str):
"""Write a scenario JSON file.
Parameters
----------
filename : str
Filename for the scenario file.
"""
if filename is not None:
self.file.file_format_version.set("major", ctsimu_supported_scenario_version["major"])
self.file.file_format_version.set("minor", ctsimu_supported_scenario_version["minor"])
write_json_file(filename=filename, dictionary=self.json_dict())
def write_metadata(self, filename:str):
"""Write a metadata JSON file for the scenario.
Parameters
----------
filename : str
Filename for the metadata file.
"""
if filename is not None:
metadata_dict = self.metadata.json_dict()
# potentially add simulation.ctsimu_scenario here:
metadata_dict["simulation"]["ctsimu_scenario"] = self.json_dict()
write_json_file(filename=filename, dictionary=metadata_dict)
def get(self, key:list) -> float | str | bool:
"""Get the current value of the parameter identified by a list of keys.
Parameters
----------
key : list
List of strings that identify the key of the requested
parameter within the CTSimU scenario structure.
Returns
-------
value : float or str or bool
Current value of the requested parameter.
"""
if isinstance(key, list):
# Special treatment for the source geometry extras: type or beam_divergence:
if len(key) > 2:
if key[0:2] == ["geometry", "source"]:
return self.source.source_geometry_extras.get(key[2:])
# Standard treatment:
if len(key) > 1:
for s in self.subgroups:
if s._name == key[0]:
return s.get(key[1:])
raise Exception(f"Error in get: key not found: {key}")
def path_of_external_file(self, filename:str) -> str:
"""Get the path of an external file referenced in the currently
imported JSON scenario.
Parameters
----------
filename : str
Possibly relative file path from JSON scenario file.
Returns
-------
abs_path : str
Absolute path to the referenced external file.
"""
if os.path.isabs(filename):
# Already absolute path?
return filename
if self.current_scenario_path is not None:
if isinstance(self.current_scenario_path, str):
json_dirname = os.path.dirname(self.current_scenario_path)
filename = f"{json_dirname}/{filename}"
# On fail, simply return the filename.
return filename
def json_dict(self) -> dict:
"""Create a CTSimU JSON dictionary from the scenario.
Returns
-------
json_dict : dict
"""
jd = dict()
jd["file"] = self.file.json_dict()
jd["environment"] = self.environment.json_dict()
jd["geometry"] = dict()
jd["geometry"]["detector"] = self.detector.geometry_dict()
jd["geometry"]["source"] = self.source.geometry_dict()
jd["geometry"]["stage"] = self.stage.geometry_dict()
jd["detector"] = self.detector.json_dict()
jd["source"] = self.source.json_dict()
jd["samples"] = []
for sample in self.samples:
jd["samples"].append(sample.json_dict())
jd["acquisition"] = self.acquisition.json_dict()
jd["materials"] = []
for material in self.materials:
jd["materials"].append(material.json_dict())
jd["simulation"] = self.simulation
return jd
def n_frames(self) -> int:
"""Number of frames in the scenario.
Returns
-------
n_frames : int
Number of frames in the scenario.
"""
# 'Frame' is in this context a projection image.
# However, if we assume frame averaging, the number of
# frames could also be: n_frames = nProjection * nFrameAverages
n_frames = self.acquisition.get("number_of_projections")
return n_frames
def current_stage_angle(self):
"""Stage rotation angle (in deg) for the current frame.
Returns
-------
stage_rotation_angle : float
Current stage rotation angle (in deg).
"""
start_angle = float(self.acquisition.get("start_angle"))
stop_angle = float(self.acquisition.get("stop_angle"))
nPositions = float(self.n_frames())
# If the final projection is taken at the stop angle
# (and not one step before), the number of positions
# has to be decreased by 1, resulting in one less
# angular step being performed.
if self.acquisition.get("include_final_angle") is True:
if nPositions > 0:
nPositions -= 1
angular_range = 0
if start_angle <= stop_angle:
angular_range = stop_angle - start_angle
else:
raise Exception("The start angle cannot be greater than the stop angle. Scan direction must be specified by the acquisition 'direction' keyword (CCW or CW).")
angular_position = start_angle
if nPositions != 0:
angular_position = start_angle + self.current_frame*angular_range/nPositions
# Mathematically negative:
if self.acquisition.get("direction") == "CW":
angular_position = -angular_position
return angular_position
def set_frame(self, frame:float=0, reconstruction:bool=False):
"""Set the current frame representation of the scenario.
Parameters
----------
frame : float
Frame number, starting at zero and usually
going to `{n_projections}-1`.
Default value: `0`
reconstruction : bool
Set the scenario geometry as seen by the reconstruction software?
If `True`, this will only obey drifts and deviations that
are `known_to_reconstruction`.
Default value: `False`
"""
self.current_frame = frame
# Number of frames:
n_frames = self.n_frames()
stage_deg = self.current_stage_angle()
stage_rot = math.radians(stage_deg)
# Update materials:
for material in self.materials:
material.set_frame(frame, n_frames, reconstruction)
# Update stage, source, detector and other parameters:
self.stage.set_frame(frame, n_frames, stage_rot, None, reconstruction)
self.source.set_frame(frame, n_frames, 0, None, reconstruction)
self.detector.set_frame(frame, n_frames, 0, None, reconstruction)
self.file.set_frame(frame, n_frames, reconstruction)
self.environment.set_frame(frame, n_frames, reconstruction)
self.acquisition.set_frame(frame, n_frames, reconstruction)
# Update samples:
stage_cs = self.stage.coordinate_system
for sample in self.samples:
sample.set_frame(frame, n_frames, 0, stage_cs, reconstruction)
def current_geometry(self) -> 'ctsimu.geometry.Geometry':
"""Return a `ctsimu.geometry.Geometry` object for the
current setup of the scenario.
Returns
-------
geometry : ctsimu.geometry.Geometry
Geometry for current frame.
"""
geo = Geometry()
geo.detector.copy_cs(self.detector.coordinate_system)
geo.source.copy_cs(self.source.coordinate_system)
geo.stage.copy_cs(self.stage.coordinate_system)
geo.detector.set_size(
pixels_u=self.detector.get("columns"),
pixels_v=self.detector.get("rows"),
pitch_u=self.detector.pixel_pitch.get("u"),
pitch_v=self.detector.pixel_pitch.get("v")
)
return geo
def write_recon_VGI(self, name:str="", volume_filename:str="", vgi_filename:str=None):
"""Write a VGI file for the reconstruction volume such that it can be loaded with VGSTUDIO."""
voxels_x = self.metadata.output.get(["tomogram", "dimensions", "x"])
voxels_y = self.metadata.output.get(["tomogram", "dimensions", "y"])
voxels_z = self.metadata.output.get(["tomogram", "dimensions", "z"])
voxelsize_x = self.metadata.output.get(["tomogram", "voxelsize", "x"])
voxelsize_y = self.metadata.output.get(["tomogram", "voxelsize", "y"])
voxelsize_z = self.metadata.output.get(["tomogram", "voxelsize", "z"])
output_datatype = self.metadata.output.get(["tomogram", "datatype"])
if output_datatype == "uint16":
dataTypeOutput = "unsigned integer"
bits = 16
datarangelower = 0
datarangeupper = -1
else:
dataTypeOutput = "float"
bits = 32
datarangelower = -1
datarangeupper = 1
vgi_content = f"""{{volume1}}
[representation]
size = {voxels_x} {voxels_y} {voxels_z}
datatype = {dataTypeOutput}
datarange = {datarangelower} {datarangeupper}
bitsperelement = {bits}
[file1]
SkipHeader = 0
FileFormat = raw
Size = {voxels_x} {voxels_y} {voxels_z}
Name = {volume_filename}
Datatype = {dataTypeOutput}
datarange = {datarangelower} {datarangeupper}
BitsPerElement = {bits}
{{volumeprimitive12}}
[geometry]
resolution = {voxelsize_x} {voxelsize_y} {voxelsize_z}
unit = mm
[volume]
volume = volume1
[description]
text = {name}"""
if vgi_filename is not None:
touch_directory(vgi_filename)
with open(vgi_filename, 'w') as f:
f.write(vgi_content)
f.close()
return vgi_content
def write_CERA_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False):
"""Write CERA reconstruction config files.
Parameters
----------
save_dir : str
Folder where to place the CERA config files. This is meant to be the
same directory where the reconstruction metadata file is located,
such that relative paths will match.
If `None` is given, a directory will be inferred:
- If only a JSON scenario file was imported to set up the scenario,
the config files will be stored in a subdirectory next to the
JSON scenario file of the following pattern:
`{json_scenario_basename}/reconstruction`
- If a reconstruction metadata file was imported, the config files
will be stored next to the metadata file.
Default value: `None`
basename : str
Base name for the created files. If `None` is given, the base
name will be inferred from the scenario's metadata.
Default value: `None`
create_vgi : bool
Write VGI file for future reconstruction volume?
"""
matrices = []
if basename is None:
# Extract base name from metadata
metadata_basename = self.metadata.get(["file", "name"])
if metadata_basename is not None:
basename = f"{metadata_basename}_recon_cera"
else:
basename = f"recon_cera"
# Projection files
n_projections = self.acquisition.get("number_of_projections")
projection_file_pattern = self.metadata.get(["output", "projections", "filename"])
projection_datatype = self.metadata.get(["output", "projections", "datatype"])
projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"])
projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"])
projection_filetype = "tiff"
if projection_file_pattern.lower().endswith(".raw"):
projection_filetype = "raw"
# Acquisition
start_angle = self.acquisition.get("start_angle")
stop_angle = self.acquisition.get("stop_angle")
total_angle = stop_angle - start_angle
for p in range(n_projections):
self.set_frame(frame=p, reconstruction=True)
# CERA projection matrix for projection p:
m = self.current_geometry().projection_matrix(mode="CERA")
matrices.append(m)
# Go back to frame zero:
self.set_frame(frame=0, reconstruction=True)
if create_vgi:
vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi")
volume_filename = f"{basename}.raw"
self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename)
create_CERA_config(
geo=self.current_geometry(),
projection_file_pattern=projection_file_pattern,
basename=basename,
save_dir=save_dir,
n_projections=n_projections,
projection_datatype=projection_datatype,
projection_filetype=projection_filetype,
projection_byteorder=projection_file_byteorder,
projection_headersize=projection_headersize,
start_angle=0, # do not compensate the scenario start angle in the reconstruction
total_angle=total_angle,
scan_direction=self.acquisition.get("direction"),
voxels_x=self.metadata.output.tomogram.dimensions.x.get(),
voxels_y=self.metadata.output.tomogram.dimensions.y.get(),
voxels_z=self.metadata.output.tomogram.dimensions.z.get(),
voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(),
voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(),
voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(),
i0max=self.metadata.output.get(["projections", "max_intensity"]),
output_datatype=convert(cera_converter["datatype"], self.metadata.output.get(["tomogram", "datatype"])),
matrices=matrices
)
def write_OpenCT_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False, variant:str='free', abspaths:bool=False):
"""Write OpenCT reconstruction config files.
Parameters
----------
save_dir : str
Folder where to place the CERA config files. This is meant to be the
same directory where the reconstruction metadata file is located,
such that relative paths will match.
If `None` is given, a directory will be inferred:
- If only a JSON scenario file was imported to set up the scenario,
the config files will be stored in a subdirectory next to the
JSON scenario file of the following pattern:
`{json_scenario_basename}/reconstruction`
- If a reconstruction metadata file was imported, the config files
will be stored next to the metadata file.
Default value: `None`
basename : str
Base name for the created config file. If `None` is given, the base
name will be inferred from the scenario's metadata.
Default value: `None`
create_vgi : bool
Write VGI file for future reconstruction volume?
variant : str
Which variant of the OpenCT file format will be created: free trajectory
or circular trajectory.
Possible values: `"free"`, `"circular"`
Default value: `"free"`
abspaths : bool
Set to `True` if absolute paths should be used in the OpenCT
config file.
Default value: `False` (relative paths)
Returns
-------
openct_dict : dict
Dictionary with the OpenCT JSON structure.
"""
matrices = []
if basename is None:
# Extract base name from metadata
metadata_basename = self.metadata.get(["file", "name"])
if metadata_basename is not None:
basename = f"{metadata_basename}_recon_openCT"
else:
basename = f"recon_openCT"
# Name of config file:
openct_config_filename = join_dir_and_filename(save_dir, f"{basename}.json")
def projections_from_pattern(json_dict:dict):
n = get_value(json_dict, ["number"]) # number of images
pattern = None
filedir = None
filename = None
filelist = list()
if n is not None:
if n > 0:
pattern = get_value(json_dict, ["filename"])
if abspaths is True:
pattern = abspath_of_referenced_file(openct_config_filename, pattern)
if pattern is not None:
filedir, filename = os.path.split(pattern)
# Generate list of projection files:
if filename is not None:
# Auto-generate projection file list.
# List of sequentially numbered projection images,
# starting at 0000.
for p in range(n):
if '%' in filename:
try:
filelist.append(filename % p)
except Exception as e:
raise Exception(f"Error in sequentially numbered filename pattern. Please give a percentage sign, followed by the number of digits and a 'd' character: 'example_%04d.tif'. You gave: '{filename}'")
else:
filelist.append(filename)
return n, filedir, filename, filelist
# Projection files
n_projections, projection_filedir, projection_filename, projection_filelist = projections_from_pattern(self.metadata.output.projections.json_dict())
projection_datatype = self.metadata.get(["output", "projections", "datatype"])
projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"])
projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"])
projection_filetype = "tiff"
if projection_filename is not None:
if projection_filename.lower().endswith(".raw"):
projection_filetype = "raw"
# Dark files; only is corrections need to be applied.
openct_dark_image = None
if not self.metadata.output.projections.dark_field.projections_corrected.get() is True:
n_darks, dark_filedir, dark_filename, dark_filelist = projections_from_pattern(self.metadata.output.projections.dark_field.json_dict())
if isinstance(dark_filelist, list):
if len(dark_filelist) > 0:
openct_dark_image = join_dir_and_filename(dark_filedir, dark_filelist[0])
# Bright files; only is corrections need to be applied.
flat_filedir = None
flat_filelist = None
if not self.metadata.output.projections.flat_field.projections_corrected.get() is True:
n_flats, flat_filedir, flat_filename, flat_filelist = projections_from_pattern(self.metadata.output.projections.flat_field.json_dict())
# Acquisition
start_angle = self.acquisition.get("start_angle")
stop_angle = self.acquisition.get("stop_angle")
total_angle = stop_angle - start_angle
for p in range(n_projections):
self.set_frame(frame=p, reconstruction=True)
# CERA projection matrix for projection p:
m = self.current_geometry().projection_matrix(mode="OpenCT")
matrices.append(m)
# Go back to frame zero:
self.set_frame(frame=0, reconstruction=True)
volume_filename = f"{basename}.img"
if create_vgi:
vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi")
self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename)
openct_dict = create_OpenCT_config(
geo=self.current_geometry(),
filename=openct_config_filename,
variant=variant,
projection_files=projection_filelist,
projection_dir=projection_filedir,
projection_datatype=projection_datatype,
projection_filetype=projection_filetype,
projection_headersize=projection_headersize,
projection_byteorder=projection_file_byteorder,
total_angle=total_angle,
scan_direction=self.acquisition.get("direction"),
matrices=matrices,
volumename=volume_filename,
voxels_x=self.metadata.output.tomogram.dimensions.x.get(),
voxels_y=self.metadata.output.tomogram.dimensions.y.get(),
voxels_z=self.metadata.output.tomogram.dimensions.z.get(),
voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(),
voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(),
voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(),
bright_image_dir=flat_filedir,
bright_images=flat_filelist,
dark_image=openct_dark_image)
return openct_dict
Sub-modules
ctsimu.scenario.acquisition
-
Collection of parameters that describe the scan acquisition.
ctsimu.scenario.detector
-
A CTSimU detector: position, orientation, size, and other parameters.
ctsimu.scenario.deviation
-
Geometrical deviation of a coordinate system, i.e. a translation or a rotation with respect to an arbitrary axis.
ctsimu.scenario.drift
-
Drift structure for a
Parameter
. ctsimu.scenario.environment
-
Collection of parameters that describe the environment.
ctsimu.scenario.file
-
Collection of parameters that describe the scenario file.
ctsimu.scenario.group
-
Groups are collections of parameters.
ctsimu.scenario.material
-
Material component: formula and mass fraction. Material: composition, density, and their drifts
ctsimu.scenario.metadata
-
CTSimU metadata of a CT scan.
ctsimu.scenario.parameter
-
Parameter value, includes unit conversion and handling of parameter drifts.
ctsimu.scenario.part
-
Parts are objects in the scene: detector, source, stage and samples.
ctsimu.scenario.sample
-
Generic sample: position and orientation, size and material properties.
ctsimu.scenario.scenevector
-
A scene vector is a 3D vector that knows its reference coordinate system. It can be converted between these coordinate systems and handles drifts.
ctsimu.scenario.source
-
A CTSimU X-ray source: position, orientation, size, and other parameters.
ctsimu.scenario.stage
-
A CTSimU sample stage: position, orientation.
Classes
class Scenario (filename: str = None, json_dict: dict = None)
-
Representation of a CTSimU scenario. Reads, writes, manages and manipulates scenarios.
Attributes
detector
:Detector
- Detector object of the scenario.
source
:Source
- X-ray source object of the scenario.
stage
:Stage
- Sample stage object of the scenario.
samples
:list
- List of samples in the scenario, each an object of class
Sample
. file
:File
- Information about the scenario file.
environment
:Environment
- Environment properties of the scenario.
acquisition
:Acquisition
- CT acquisition parameters.
materials
:list
- List of materials used in the scenario, each an object of
class
Material
. simulation
:dict
- Dictionary of simulation software-specific properties from the scenario file.
metadata
:Metadata
- Metadata information about projection images and the reconstruction volume. This information depends on the output of the CT scanner or simulation software, not on the scenario itself.
The scenario can optionally constructed by passing the path to a scenario file or by passing a Python dictionary that contains scenario information:
Parameters
filename
:str
, optional- Path to a CTSimU scenario file.
json_dict
:dict
, optional- A Python dictionary containing a CTSimU scenario. The dictionary must obey the same structure as a CTSimU JSON scenario file.
Expand source code
class Scenario: """Representation of a CTSimU scenario. Reads, writes, manages and manipulates scenarios. Attributes ---------- detector : ctsimu.scenario.detector.Detector Detector object of the scenario. source : ctsimu.scenario.source.Source X-ray source object of the scenario. stage : ctsimu.scenario.stage.Stage Sample stage object of the scenario. samples : list List of samples in the scenario, each an object of class `ctsimu.scenario.sample.Sample`. file : ctsimu.scenario.file.File Information about the scenario file. environment : ctsimu.scenario.environment.Environment Environment properties of the scenario. acquisition : ctsimu.scenario.acquisition.Acquisition CT acquisition parameters. materials : list List of materials used in the scenario, each an object of class `ctsimu.scenario.material.Material`. simulation : dict Dictionary of simulation software-specific properties from the scenario file. metadata : ctsimu.scenario.metadata.Metadata Metadata information about projection images and the reconstruction volume. This information depends on the output of the CT scanner or simulation software, not on the scenario itself. """ def __init__(self, filename:str=None, json_dict:dict=None): """The scenario can optionally constructed by passing the path to a scenario file or by passing a Python dictionary that contains scenario information: Parameters ---------- filename : str, optional Path to a [CTSimU scenario] file. [CTSimU scenario]: https://bamresearch.github.io/ctsimu-scenarios json_dict : dict, optional A Python dictionary containing a CTSimU scenario. The dictionary must obey the same structure as a CTSimU JSON scenario file. """ self.detector = Detector(_root=self) self.source = Source(_root=self) self.stage = Stage(_root=self) self.samples = list() self.file = File(_root=self) self.environment = Environment(_root=self) self.acquisition = Acquisition(_root=self) self.materials = list() self.simulation = None # simply imported as dict # CTSimU metadata structure: # Necessary for generation of reconstruction configs. self.metadata = Metadata(_root=self) # Subgroups are necessary for the 'get' and 'set' command: self.subgroups = [ self.detector, self.source, self.stage, self.file, self.environment, self.acquisition, self.metadata ] self.current_frame = 0 self.current_scenario_path = None self.current_scenario_file = None self.current_scenario_basename = None self.current_scenario_directory = None self.current_metadata_path = None self.current_metadata_file = None self.current_metadata_basename = None self.current_metadata_directory = None self.metadata_is_set = False if filename is not None: self.read(filename=filename) elif json_dict is not None: self.read(json_dict=json_dict) def read(self, filename:str=None, json_dict:dict=None): """Import a CTSimU scenario from a file or a given scenario dictionary. Parameters ---------- filename : str Path to a CTSimU scenario file. Default value: `None` json_dict : dict Provide a dictionary with a scenario structure instead of a file. Default value: `None` """ self.current_scenario_path = None self.reset() if filename is not None: json_dict = read_json_file(filename=filename) self.current_scenario_path = filename self.current_scenario_directory = os.path.dirname(filename) self.current_scenario_file = os.path.basename(filename) self.current_scenario_basename, extension = os.path.splitext(self.current_scenario_file) elif not isinstance(json_dict, dict): raise Exception("Scenario: read() function expects either a filename as a string or a CTSimU JSON dictionary as a Python dict.") return False # If a file is read, we want to make sure that it is a valid # and supported scenario file: if isinstance(json_dict, dict): file_type = get_value(json_dict, ["file", "file_type"]) if file_type != "CTSimU Scenario": raise Exception(f"Invalid scenario structure: the string 'CTSimU Scenario' was not found in 'file.file_type' in the scenario file {file}.") file_format_version = get_value(json_dict, ["file", "file_format_version"]) if not is_version_supported(ctsimu_supported_scenario_version, file_format_version): raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.") else: raise Exception(f"Error when reading the scenario file: {filename}. read_json_file did not return a Python dictionary.") self.detector.set_from_json(json_dict) self.source.set_from_json(json_dict) self.stage.set_from_json(json_dict) json_samples = json_extract(json_dict, ["samples"]) if json_samples is not None: for json_sample in json_samples: s = Sample(_root=self) s.set_from_json(json_sample, self.stage.coordinate_system) self.samples.append(s) self.file.set_from_json(json_extract(json_dict, [self.file._name])) self.environment.set_from_json(json_extract(json_dict, [self.environment._name])) self.acquisition.set_from_json(json_extract(json_dict, [self.acquisition._name])) self.simulation = json_extract(json_dict, ["simulation"]) json_materials = json_extract(json_dict, ["materials"]) for json_material in json_materials: m = Material(_root=self) m.set_from_json(json_material) self.materials.append(m) self.set_frame(0, reconstruction=False) if not self.metadata_is_set: self.create_default_metadata() def reset(self): """Reset scenario: delete all drifts, deviations and sample and material information.""" for group in self.subgroups: group.reset() def reset_metadata(self): """Reset scenario's metadata information.""" self.current_metadata_path = None self.current_metadata_file = None self.current_metadata_basename = None self.current_metadata_directory = None self.metadata_is_set = False # Create new, empty metadata: self.metadata = Metadata() def read_metadata(self, filename:str=None, json_dict:dict=None, import_referenced_scenario:bool=False): """Import metadata from a CTSimU metadata file or a given metadata dictionary. Parameters ---------- filename : str Path to a CTSimU metadata file. Default value: `None` json_dict : dict Provide a dictionary with a metadata structure instead of a file. Default value: `None` import_referenced_scenario : bool Import the scenario JSON file that's referenced in the metadata file? Generates a warning if this fails. The scenario definition will be searched at two locations in the following order: 1. Try to read from external file defined by `acquisition_geometry.path_to_CTSimU_JSON`. 2. Try to read embedded scenario definition from `simulation.ctsimu_scenario`. Note that external drift files specified in the scenario will likely fail to load. An error will be issued in this case. Default value: `False` """ if filename is not None: json_dict = read_json_file(filename=filename) self.current_metadata_path = filename self.current_metadata_directory = os.path.dirname(filename) self.current_metadata_file = os.path.basename(filename) self.current_metadata_basename, extension = os.path.splitext(self.current_metadata_file) # If a file is read, we want to make sure that it is a valid # and supported metadata file: if isinstance(json_dict, dict): file_type = get_value(json_dict, ["file", "file_type"]) if file_type != "CTSimU Metadata": raise Exception(f"Invalid metadata structure: the string 'CTSimU Metadata' was not found in 'file.file_type' in the metadata file {file}.") fileformatversion = get_value(json_dict, ["file", "file_format_version"]) if not is_version_supported(ctsimu_supported_metadata_version, fileformatversion): raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.") else: raise Exception(f"Error when reading the metadata file: {filename}. read_json_file did not return a Python dictionary.") # If we get a `json_dict` as function parameter, we do not # test for a valid version because reduced/simplified metadata # structures should be supported as well. self.metadata.set_from_json(json_dict) self.metadata_is_set = True if import_referenced_scenario: # Import the scenario that's referenced in the metadata file. ref_file = self.metadata.get(["acquisition_geometry", "path_to_CTSimU_JSON"]) import_success = False try: if (ref_file is not None) and (ref_file != ""): if isinstance(ref_file, str): ref_file = abspath_of_referenced_file(self.current_metadata_path, ref_file) else: raise Exception("read_metadata: path_to_CTSimU_JSON is not a string.") # Try to import scenario: self.read(filename=ref_file) import_success = True except Exception as e: warnings.warn(str(e)) import_success = False if not import_success: # Try to import the embedded scenario structure. if json_exists_and_not_null(json_dict, ["simulation", "ctsimu_scenario"]): self.read(json_dict=json_dict["simulation"]["ctsimu_scenario"]) import_success = True # Create default metadata in case the original # metadata file did not contain all information that's needed: self.create_default_metadata() # Re-import metadata: self.metadata.set_from_json(json_dict) def create_default_metadata(self): """Set default metadata from scenario information, to use if no metadata file is available.""" self.reset_metadata() geo = self.current_geometry() cera_parameters = geo.get_CERA_standard_circular_parameters() # Basename: if self.current_scenario_basename is not None: self.current_metadata_basename = self.current_scenario_basename basename = self.current_metadata_basename n_projections = self.acquisition.get("number_of_projections") projection_filename = f"{basename}_{counter_format(n_projections)}.raw" # Prepare filename for dark fields: n_darks = self.acquisition.dark_field.get("number") dark_filename = None if n_darks is not None: if n_darks > 0: if n_darks > 1: dark_filename = f"{basename}_dark_{counter_format(n_darks)}.raw" else: dark_filename = f"{basename}_dark.raw" # Prepare filename for flat fields: n_flats = self.acquisition.flat_field.get("number") flat_filename = None if n_flats is not None: if n_flats > 0: if n_flats > 1: flat_filename = f"{basename}_flat_{counter_format(n_flats)}.raw" else: flat_filename = f"{basename}_flat.raw" n_cols = self.detector.get("columns") n_rows = self.detector.get("rows") pixel_size_u = self.detector.pixel_pitch.get("u") pixel_size_v = self.detector.pixel_pitch.get("v") voxels_x = n_cols voxels_y = n_cols voxels_z = n_rows voxelsize_x = cera_parameters["voxelsize"]["x"] voxelsize_y = cera_parameters["voxelsize"]["y"] voxelsize_z = cera_parameters["voxelsize"]["z"] now = datetime.now() metadata = { "file": { "name": basename, "description": self.file.get("description"), "contact": self.file.get("contact"), "date_created": now.strftime("%Y-%m-%d"), "date_changed": now.strftime("%Y-%m-%d"), "file_type": "CTSimU Metadata", "file_format_version": { "major": ctsimu_supported_metadata_version["major"], "minor": ctsimu_supported_metadata_version["minor"] } }, "output": { "system": None, "date_measured": None, "projections": { "filename": projection_filename, "number": n_projections, "frame_average": self.acquisition.get("frame_average"), "max_intensity": self.detector.get(["gray_value", "imax"]), "datatype": "uint16", "byteorder": "little", "headersize": { "file": 0, "image": 0 }, "dimensions": { "x": {"value": n_cols, "unit": "px"}, "y": {"value": n_rows, "unit": "px"} }, "pixelsize": { "x": {"value": pixel_size_u, "unit": "mm"}, "y": {"value": pixel_size_v, "unit": "mm"} }, "dark_field": { "number": n_darks, "frame_average": self.acquisition.dark_field.get("frame_average"), "filename": dark_filename, "projections_corrected": False }, "flat_field": { "number": n_flats, "frame_average": self.acquisition.flat_field.get("frame_average"), "filename": flat_filename, "projections_corrected": False }, "bad_pixel_map": { "filename": None, "projections_corrected": False } }, "tomogram": { "filename": f"{basename}_recon.raw", "datatype": "float32", "byteorder": "little", "headersize": { "file": 0, "image": 0 }, "dimensions": { "x": {"value": voxels_x, "unit": "px"}, "y": {"value": voxels_y, "unit": "px"}, "z": {"value": voxels_z, "unit": "px"} }, "voxelsize": { "x": {"value": voxelsize_x, "unit": "mm"}, "y": {"value": voxelsize_y, "unit": "mm"}, "z": {"value": voxelsize_z, "unit": "mm"} } } }, "acquisition_geometry": { "path_to_CTSimU_JSON": self.current_scenario_path }, "reconstruction": { "software": None, "settings": { } }, "simulation": { "ctsimu_scenario": None } } self.read_metadata(json_dict=metadata, import_referenced_scenario=False) def write(self, filename:str): """Write a scenario JSON file. Parameters ---------- filename : str Filename for the scenario file. """ if filename is not None: self.file.file_format_version.set("major", ctsimu_supported_scenario_version["major"]) self.file.file_format_version.set("minor", ctsimu_supported_scenario_version["minor"]) write_json_file(filename=filename, dictionary=self.json_dict()) def write_metadata(self, filename:str): """Write a metadata JSON file for the scenario. Parameters ---------- filename : str Filename for the metadata file. """ if filename is not None: metadata_dict = self.metadata.json_dict() # potentially add simulation.ctsimu_scenario here: metadata_dict["simulation"]["ctsimu_scenario"] = self.json_dict() write_json_file(filename=filename, dictionary=metadata_dict) def get(self, key:list) -> float | str | bool: """Get the current value of the parameter identified by a list of keys. Parameters ---------- key : list List of strings that identify the key of the requested parameter within the CTSimU scenario structure. Returns ------- value : float or str or bool Current value of the requested parameter. """ if isinstance(key, list): # Special treatment for the source geometry extras: type or beam_divergence: if len(key) > 2: if key[0:2] == ["geometry", "source"]: return self.source.source_geometry_extras.get(key[2:]) # Standard treatment: if len(key) > 1: for s in self.subgroups: if s._name == key[0]: return s.get(key[1:]) raise Exception(f"Error in get: key not found: {key}") def path_of_external_file(self, filename:str) -> str: """Get the path of an external file referenced in the currently imported JSON scenario. Parameters ---------- filename : str Possibly relative file path from JSON scenario file. Returns ------- abs_path : str Absolute path to the referenced external file. """ if os.path.isabs(filename): # Already absolute path? return filename if self.current_scenario_path is not None: if isinstance(self.current_scenario_path, str): json_dirname = os.path.dirname(self.current_scenario_path) filename = f"{json_dirname}/{filename}" # On fail, simply return the filename. return filename def json_dict(self) -> dict: """Create a CTSimU JSON dictionary from the scenario. Returns ------- json_dict : dict """ jd = dict() jd["file"] = self.file.json_dict() jd["environment"] = self.environment.json_dict() jd["geometry"] = dict() jd["geometry"]["detector"] = self.detector.geometry_dict() jd["geometry"]["source"] = self.source.geometry_dict() jd["geometry"]["stage"] = self.stage.geometry_dict() jd["detector"] = self.detector.json_dict() jd["source"] = self.source.json_dict() jd["samples"] = [] for sample in self.samples: jd["samples"].append(sample.json_dict()) jd["acquisition"] = self.acquisition.json_dict() jd["materials"] = [] for material in self.materials: jd["materials"].append(material.json_dict()) jd["simulation"] = self.simulation return jd def n_frames(self) -> int: """Number of frames in the scenario. Returns ------- n_frames : int Number of frames in the scenario. """ # 'Frame' is in this context a projection image. # However, if we assume frame averaging, the number of # frames could also be: n_frames = nProjection * nFrameAverages n_frames = self.acquisition.get("number_of_projections") return n_frames def current_stage_angle(self): """Stage rotation angle (in deg) for the current frame. Returns ------- stage_rotation_angle : float Current stage rotation angle (in deg). """ start_angle = float(self.acquisition.get("start_angle")) stop_angle = float(self.acquisition.get("stop_angle")) nPositions = float(self.n_frames()) # If the final projection is taken at the stop angle # (and not one step before), the number of positions # has to be decreased by 1, resulting in one less # angular step being performed. if self.acquisition.get("include_final_angle") is True: if nPositions > 0: nPositions -= 1 angular_range = 0 if start_angle <= stop_angle: angular_range = stop_angle - start_angle else: raise Exception("The start angle cannot be greater than the stop angle. Scan direction must be specified by the acquisition 'direction' keyword (CCW or CW).") angular_position = start_angle if nPositions != 0: angular_position = start_angle + self.current_frame*angular_range/nPositions # Mathematically negative: if self.acquisition.get("direction") == "CW": angular_position = -angular_position return angular_position def set_frame(self, frame:float=0, reconstruction:bool=False): """Set the current frame representation of the scenario. Parameters ---------- frame : float Frame number, starting at zero and usually going to `{n_projections}-1`. Default value: `0` reconstruction : bool Set the scenario geometry as seen by the reconstruction software? If `True`, this will only obey drifts and deviations that are `known_to_reconstruction`. Default value: `False` """ self.current_frame = frame # Number of frames: n_frames = self.n_frames() stage_deg = self.current_stage_angle() stage_rot = math.radians(stage_deg) # Update materials: for material in self.materials: material.set_frame(frame, n_frames, reconstruction) # Update stage, source, detector and other parameters: self.stage.set_frame(frame, n_frames, stage_rot, None, reconstruction) self.source.set_frame(frame, n_frames, 0, None, reconstruction) self.detector.set_frame(frame, n_frames, 0, None, reconstruction) self.file.set_frame(frame, n_frames, reconstruction) self.environment.set_frame(frame, n_frames, reconstruction) self.acquisition.set_frame(frame, n_frames, reconstruction) # Update samples: stage_cs = self.stage.coordinate_system for sample in self.samples: sample.set_frame(frame, n_frames, 0, stage_cs, reconstruction) def current_geometry(self) -> 'ctsimu.geometry.Geometry': """Return a `ctsimu.geometry.Geometry` object for the current setup of the scenario. Returns ------- geometry : ctsimu.geometry.Geometry Geometry for current frame. """ geo = Geometry() geo.detector.copy_cs(self.detector.coordinate_system) geo.source.copy_cs(self.source.coordinate_system) geo.stage.copy_cs(self.stage.coordinate_system) geo.detector.set_size( pixels_u=self.detector.get("columns"), pixels_v=self.detector.get("rows"), pitch_u=self.detector.pixel_pitch.get("u"), pitch_v=self.detector.pixel_pitch.get("v") ) return geo def write_recon_VGI(self, name:str="", volume_filename:str="", vgi_filename:str=None): """Write a VGI file for the reconstruction volume such that it can be loaded with VGSTUDIO.""" voxels_x = self.metadata.output.get(["tomogram", "dimensions", "x"]) voxels_y = self.metadata.output.get(["tomogram", "dimensions", "y"]) voxels_z = self.metadata.output.get(["tomogram", "dimensions", "z"]) voxelsize_x = self.metadata.output.get(["tomogram", "voxelsize", "x"]) voxelsize_y = self.metadata.output.get(["tomogram", "voxelsize", "y"]) voxelsize_z = self.metadata.output.get(["tomogram", "voxelsize", "z"]) output_datatype = self.metadata.output.get(["tomogram", "datatype"]) if output_datatype == "uint16": dataTypeOutput = "unsigned integer" bits = 16 datarangelower = 0 datarangeupper = -1 else: dataTypeOutput = "float" bits = 32 datarangelower = -1 datarangeupper = 1 vgi_content = f"""{{volume1}} [representation] size = {voxels_x} {voxels_y} {voxels_z} datatype = {dataTypeOutput} datarange = {datarangelower} {datarangeupper} bitsperelement = {bits} [file1] SkipHeader = 0 FileFormat = raw Size = {voxels_x} {voxels_y} {voxels_z} Name = {volume_filename} Datatype = {dataTypeOutput} datarange = {datarangelower} {datarangeupper} BitsPerElement = {bits} {{volumeprimitive12}} [geometry] resolution = {voxelsize_x} {voxelsize_y} {voxelsize_z} unit = mm [volume] volume = volume1 [description] text = {name}""" if vgi_filename is not None: touch_directory(vgi_filename) with open(vgi_filename, 'w') as f: f.write(vgi_content) f.close() return vgi_content def write_CERA_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False): """Write CERA reconstruction config files. Parameters ---------- save_dir : str Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match. If `None` is given, a directory will be inferred: - If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern: `{json_scenario_basename}/reconstruction` - If a reconstruction metadata file was imported, the config files will be stored next to the metadata file. Default value: `None` basename : str Base name for the created files. If `None` is given, the base name will be inferred from the scenario's metadata. Default value: `None` create_vgi : bool Write VGI file for future reconstruction volume? """ matrices = [] if basename is None: # Extract base name from metadata metadata_basename = self.metadata.get(["file", "name"]) if metadata_basename is not None: basename = f"{metadata_basename}_recon_cera" else: basename = f"recon_cera" # Projection files n_projections = self.acquisition.get("number_of_projections") projection_file_pattern = self.metadata.get(["output", "projections", "filename"]) projection_datatype = self.metadata.get(["output", "projections", "datatype"]) projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"]) projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"]) projection_filetype = "tiff" if projection_file_pattern.lower().endswith(".raw"): projection_filetype = "raw" # Acquisition start_angle = self.acquisition.get("start_angle") stop_angle = self.acquisition.get("stop_angle") total_angle = stop_angle - start_angle for p in range(n_projections): self.set_frame(frame=p, reconstruction=True) # CERA projection matrix for projection p: m = self.current_geometry().projection_matrix(mode="CERA") matrices.append(m) # Go back to frame zero: self.set_frame(frame=0, reconstruction=True) if create_vgi: vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi") volume_filename = f"{basename}.raw" self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename) create_CERA_config( geo=self.current_geometry(), projection_file_pattern=projection_file_pattern, basename=basename, save_dir=save_dir, n_projections=n_projections, projection_datatype=projection_datatype, projection_filetype=projection_filetype, projection_byteorder=projection_file_byteorder, projection_headersize=projection_headersize, start_angle=0, # do not compensate the scenario start angle in the reconstruction total_angle=total_angle, scan_direction=self.acquisition.get("direction"), voxels_x=self.metadata.output.tomogram.dimensions.x.get(), voxels_y=self.metadata.output.tomogram.dimensions.y.get(), voxels_z=self.metadata.output.tomogram.dimensions.z.get(), voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(), voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(), voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(), i0max=self.metadata.output.get(["projections", "max_intensity"]), output_datatype=convert(cera_converter["datatype"], self.metadata.output.get(["tomogram", "datatype"])), matrices=matrices ) def write_OpenCT_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False, variant:str='free', abspaths:bool=False): """Write OpenCT reconstruction config files. Parameters ---------- save_dir : str Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match. If `None` is given, a directory will be inferred: - If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern: `{json_scenario_basename}/reconstruction` - If a reconstruction metadata file was imported, the config files will be stored next to the metadata file. Default value: `None` basename : str Base name for the created config file. If `None` is given, the base name will be inferred from the scenario's metadata. Default value: `None` create_vgi : bool Write VGI file for future reconstruction volume? variant : str Which variant of the OpenCT file format will be created: free trajectory or circular trajectory. Possible values: `"free"`, `"circular"` Default value: `"free"` abspaths : bool Set to `True` if absolute paths should be used in the OpenCT config file. Default value: `False` (relative paths) Returns ------- openct_dict : dict Dictionary with the OpenCT JSON structure. """ matrices = [] if basename is None: # Extract base name from metadata metadata_basename = self.metadata.get(["file", "name"]) if metadata_basename is not None: basename = f"{metadata_basename}_recon_openCT" else: basename = f"recon_openCT" # Name of config file: openct_config_filename = join_dir_and_filename(save_dir, f"{basename}.json") def projections_from_pattern(json_dict:dict): n = get_value(json_dict, ["number"]) # number of images pattern = None filedir = None filename = None filelist = list() if n is not None: if n > 0: pattern = get_value(json_dict, ["filename"]) if abspaths is True: pattern = abspath_of_referenced_file(openct_config_filename, pattern) if pattern is not None: filedir, filename = os.path.split(pattern) # Generate list of projection files: if filename is not None: # Auto-generate projection file list. # List of sequentially numbered projection images, # starting at 0000. for p in range(n): if '%' in filename: try: filelist.append(filename % p) except Exception as e: raise Exception(f"Error in sequentially numbered filename pattern. Please give a percentage sign, followed by the number of digits and a 'd' character: 'example_%04d.tif'. You gave: '{filename}'") else: filelist.append(filename) return n, filedir, filename, filelist # Projection files n_projections, projection_filedir, projection_filename, projection_filelist = projections_from_pattern(self.metadata.output.projections.json_dict()) projection_datatype = self.metadata.get(["output", "projections", "datatype"]) projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"]) projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"]) projection_filetype = "tiff" if projection_filename is not None: if projection_filename.lower().endswith(".raw"): projection_filetype = "raw" # Dark files; only is corrections need to be applied. openct_dark_image = None if not self.metadata.output.projections.dark_field.projections_corrected.get() is True: n_darks, dark_filedir, dark_filename, dark_filelist = projections_from_pattern(self.metadata.output.projections.dark_field.json_dict()) if isinstance(dark_filelist, list): if len(dark_filelist) > 0: openct_dark_image = join_dir_and_filename(dark_filedir, dark_filelist[0]) # Bright files; only is corrections need to be applied. flat_filedir = None flat_filelist = None if not self.metadata.output.projections.flat_field.projections_corrected.get() is True: n_flats, flat_filedir, flat_filename, flat_filelist = projections_from_pattern(self.metadata.output.projections.flat_field.json_dict()) # Acquisition start_angle = self.acquisition.get("start_angle") stop_angle = self.acquisition.get("stop_angle") total_angle = stop_angle - start_angle for p in range(n_projections): self.set_frame(frame=p, reconstruction=True) # CERA projection matrix for projection p: m = self.current_geometry().projection_matrix(mode="OpenCT") matrices.append(m) # Go back to frame zero: self.set_frame(frame=0, reconstruction=True) volume_filename = f"{basename}.img" if create_vgi: vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi") self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename) openct_dict = create_OpenCT_config( geo=self.current_geometry(), filename=openct_config_filename, variant=variant, projection_files=projection_filelist, projection_dir=projection_filedir, projection_datatype=projection_datatype, projection_filetype=projection_filetype, projection_headersize=projection_headersize, projection_byteorder=projection_file_byteorder, total_angle=total_angle, scan_direction=self.acquisition.get("direction"), matrices=matrices, volumename=volume_filename, voxels_x=self.metadata.output.tomogram.dimensions.x.get(), voxels_y=self.metadata.output.tomogram.dimensions.y.get(), voxels_z=self.metadata.output.tomogram.dimensions.z.get(), voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(), voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(), voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(), bright_image_dir=flat_filedir, bright_images=flat_filelist, dark_image=openct_dark_image) return openct_dict
Methods
def create_default_metadata(self)
-
Set default metadata from scenario information, to use if no metadata file is available.
Expand source code
def create_default_metadata(self): """Set default metadata from scenario information, to use if no metadata file is available.""" self.reset_metadata() geo = self.current_geometry() cera_parameters = geo.get_CERA_standard_circular_parameters() # Basename: if self.current_scenario_basename is not None: self.current_metadata_basename = self.current_scenario_basename basename = self.current_metadata_basename n_projections = self.acquisition.get("number_of_projections") projection_filename = f"{basename}_{counter_format(n_projections)}.raw" # Prepare filename for dark fields: n_darks = self.acquisition.dark_field.get("number") dark_filename = None if n_darks is not None: if n_darks > 0: if n_darks > 1: dark_filename = f"{basename}_dark_{counter_format(n_darks)}.raw" else: dark_filename = f"{basename}_dark.raw" # Prepare filename for flat fields: n_flats = self.acquisition.flat_field.get("number") flat_filename = None if n_flats is not None: if n_flats > 0: if n_flats > 1: flat_filename = f"{basename}_flat_{counter_format(n_flats)}.raw" else: flat_filename = f"{basename}_flat.raw" n_cols = self.detector.get("columns") n_rows = self.detector.get("rows") pixel_size_u = self.detector.pixel_pitch.get("u") pixel_size_v = self.detector.pixel_pitch.get("v") voxels_x = n_cols voxels_y = n_cols voxels_z = n_rows voxelsize_x = cera_parameters["voxelsize"]["x"] voxelsize_y = cera_parameters["voxelsize"]["y"] voxelsize_z = cera_parameters["voxelsize"]["z"] now = datetime.now() metadata = { "file": { "name": basename, "description": self.file.get("description"), "contact": self.file.get("contact"), "date_created": now.strftime("%Y-%m-%d"), "date_changed": now.strftime("%Y-%m-%d"), "file_type": "CTSimU Metadata", "file_format_version": { "major": ctsimu_supported_metadata_version["major"], "minor": ctsimu_supported_metadata_version["minor"] } }, "output": { "system": None, "date_measured": None, "projections": { "filename": projection_filename, "number": n_projections, "frame_average": self.acquisition.get("frame_average"), "max_intensity": self.detector.get(["gray_value", "imax"]), "datatype": "uint16", "byteorder": "little", "headersize": { "file": 0, "image": 0 }, "dimensions": { "x": {"value": n_cols, "unit": "px"}, "y": {"value": n_rows, "unit": "px"} }, "pixelsize": { "x": {"value": pixel_size_u, "unit": "mm"}, "y": {"value": pixel_size_v, "unit": "mm"} }, "dark_field": { "number": n_darks, "frame_average": self.acquisition.dark_field.get("frame_average"), "filename": dark_filename, "projections_corrected": False }, "flat_field": { "number": n_flats, "frame_average": self.acquisition.flat_field.get("frame_average"), "filename": flat_filename, "projections_corrected": False }, "bad_pixel_map": { "filename": None, "projections_corrected": False } }, "tomogram": { "filename": f"{basename}_recon.raw", "datatype": "float32", "byteorder": "little", "headersize": { "file": 0, "image": 0 }, "dimensions": { "x": {"value": voxels_x, "unit": "px"}, "y": {"value": voxels_y, "unit": "px"}, "z": {"value": voxels_z, "unit": "px"} }, "voxelsize": { "x": {"value": voxelsize_x, "unit": "mm"}, "y": {"value": voxelsize_y, "unit": "mm"}, "z": {"value": voxelsize_z, "unit": "mm"} } } }, "acquisition_geometry": { "path_to_CTSimU_JSON": self.current_scenario_path }, "reconstruction": { "software": None, "settings": { } }, "simulation": { "ctsimu_scenario": None } } self.read_metadata(json_dict=metadata, import_referenced_scenario=False)
def current_geometry(self) ‑> Geometry
-
Return a
Geometry
object for the current setup of the scenario.Returns
geometry
:Geometry
- Geometry for current frame.
Expand source code
def current_geometry(self) -> 'ctsimu.geometry.Geometry': """Return a `ctsimu.geometry.Geometry` object for the current setup of the scenario. Returns ------- geometry : ctsimu.geometry.Geometry Geometry for current frame. """ geo = Geometry() geo.detector.copy_cs(self.detector.coordinate_system) geo.source.copy_cs(self.source.coordinate_system) geo.stage.copy_cs(self.stage.coordinate_system) geo.detector.set_size( pixels_u=self.detector.get("columns"), pixels_v=self.detector.get("rows"), pitch_u=self.detector.pixel_pitch.get("u"), pitch_v=self.detector.pixel_pitch.get("v") ) return geo
def current_stage_angle(self)
-
Stage rotation angle (in deg) for the current frame.
Returns
stage_rotation_angle
:float
- Current stage rotation angle (in deg).
Expand source code
def current_stage_angle(self): """Stage rotation angle (in deg) for the current frame. Returns ------- stage_rotation_angle : float Current stage rotation angle (in deg). """ start_angle = float(self.acquisition.get("start_angle")) stop_angle = float(self.acquisition.get("stop_angle")) nPositions = float(self.n_frames()) # If the final projection is taken at the stop angle # (and not one step before), the number of positions # has to be decreased by 1, resulting in one less # angular step being performed. if self.acquisition.get("include_final_angle") is True: if nPositions > 0: nPositions -= 1 angular_range = 0 if start_angle <= stop_angle: angular_range = stop_angle - start_angle else: raise Exception("The start angle cannot be greater than the stop angle. Scan direction must be specified by the acquisition 'direction' keyword (CCW or CW).") angular_position = start_angle if nPositions != 0: angular_position = start_angle + self.current_frame*angular_range/nPositions # Mathematically negative: if self.acquisition.get("direction") == "CW": angular_position = -angular_position return angular_position
def get(self, key: list) ‑> float | str | bool
-
Get the current value of the parameter identified by a list of keys.
Parameters
key
:list
- List of strings that identify the key of the requested parameter within the CTSimU scenario structure.
Returns
value
:float
orstr
orbool
- Current value of the requested parameter.
Expand source code
def get(self, key:list) -> float | str | bool: """Get the current value of the parameter identified by a list of keys. Parameters ---------- key : list List of strings that identify the key of the requested parameter within the CTSimU scenario structure. Returns ------- value : float or str or bool Current value of the requested parameter. """ if isinstance(key, list): # Special treatment for the source geometry extras: type or beam_divergence: if len(key) > 2: if key[0:2] == ["geometry", "source"]: return self.source.source_geometry_extras.get(key[2:]) # Standard treatment: if len(key) > 1: for s in self.subgroups: if s._name == key[0]: return s.get(key[1:]) raise Exception(f"Error in get: key not found: {key}")
def json_dict(self) ‑> dict
-
Create a CTSimU JSON dictionary from the scenario.
Returns
json_dict
:dict
Expand source code
def json_dict(self) -> dict: """Create a CTSimU JSON dictionary from the scenario. Returns ------- json_dict : dict """ jd = dict() jd["file"] = self.file.json_dict() jd["environment"] = self.environment.json_dict() jd["geometry"] = dict() jd["geometry"]["detector"] = self.detector.geometry_dict() jd["geometry"]["source"] = self.source.geometry_dict() jd["geometry"]["stage"] = self.stage.geometry_dict() jd["detector"] = self.detector.json_dict() jd["source"] = self.source.json_dict() jd["samples"] = [] for sample in self.samples: jd["samples"].append(sample.json_dict()) jd["acquisition"] = self.acquisition.json_dict() jd["materials"] = [] for material in self.materials: jd["materials"].append(material.json_dict()) jd["simulation"] = self.simulation return jd
def n_frames(self) ‑> int
-
Number of frames in the scenario.
Returns
n_frames
:int
- Number of frames in the scenario.
Expand source code
def n_frames(self) -> int: """Number of frames in the scenario. Returns ------- n_frames : int Number of frames in the scenario. """ # 'Frame' is in this context a projection image. # However, if we assume frame averaging, the number of # frames could also be: n_frames = nProjection * nFrameAverages n_frames = self.acquisition.get("number_of_projections") return n_frames
def path_of_external_file(self, filename: str) ‑> str
-
Get the path of an external file referenced in the currently imported JSON scenario.
Parameters
filename
:str
- Possibly relative file path from JSON scenario file.
Returns
abs_path
:str
- Absolute path to the referenced external file.
Expand source code
def path_of_external_file(self, filename:str) -> str: """Get the path of an external file referenced in the currently imported JSON scenario. Parameters ---------- filename : str Possibly relative file path from JSON scenario file. Returns ------- abs_path : str Absolute path to the referenced external file. """ if os.path.isabs(filename): # Already absolute path? return filename if self.current_scenario_path is not None: if isinstance(self.current_scenario_path, str): json_dirname = os.path.dirname(self.current_scenario_path) filename = f"{json_dirname}/{filename}" # On fail, simply return the filename. return filename
def read(self, filename: str = None, json_dict: dict = None)
-
Import a CTSimU scenario from a file or a given scenario dictionary.
Parameters
filename
:str
-
Path to a CTSimU scenario file.
Default value:
None
json_dict
:dict
-
Provide a dictionary with a scenario structure instead of a file.
Default value:
None
Expand source code
def read(self, filename:str=None, json_dict:dict=None): """Import a CTSimU scenario from a file or a given scenario dictionary. Parameters ---------- filename : str Path to a CTSimU scenario file. Default value: `None` json_dict : dict Provide a dictionary with a scenario structure instead of a file. Default value: `None` """ self.current_scenario_path = None self.reset() if filename is not None: json_dict = read_json_file(filename=filename) self.current_scenario_path = filename self.current_scenario_directory = os.path.dirname(filename) self.current_scenario_file = os.path.basename(filename) self.current_scenario_basename, extension = os.path.splitext(self.current_scenario_file) elif not isinstance(json_dict, dict): raise Exception("Scenario: read() function expects either a filename as a string or a CTSimU JSON dictionary as a Python dict.") return False # If a file is read, we want to make sure that it is a valid # and supported scenario file: if isinstance(json_dict, dict): file_type = get_value(json_dict, ["file", "file_type"]) if file_type != "CTSimU Scenario": raise Exception(f"Invalid scenario structure: the string 'CTSimU Scenario' was not found in 'file.file_type' in the scenario file {file}.") file_format_version = get_value(json_dict, ["file", "file_format_version"]) if not is_version_supported(ctsimu_supported_scenario_version, file_format_version): raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.") else: raise Exception(f"Error when reading the scenario file: {filename}. read_json_file did not return a Python dictionary.") self.detector.set_from_json(json_dict) self.source.set_from_json(json_dict) self.stage.set_from_json(json_dict) json_samples = json_extract(json_dict, ["samples"]) if json_samples is not None: for json_sample in json_samples: s = Sample(_root=self) s.set_from_json(json_sample, self.stage.coordinate_system) self.samples.append(s) self.file.set_from_json(json_extract(json_dict, [self.file._name])) self.environment.set_from_json(json_extract(json_dict, [self.environment._name])) self.acquisition.set_from_json(json_extract(json_dict, [self.acquisition._name])) self.simulation = json_extract(json_dict, ["simulation"]) json_materials = json_extract(json_dict, ["materials"]) for json_material in json_materials: m = Material(_root=self) m.set_from_json(json_material) self.materials.append(m) self.set_frame(0, reconstruction=False) if not self.metadata_is_set: self.create_default_metadata()
def read_metadata(self, filename: str = None, json_dict: dict = None, import_referenced_scenario: bool = False)
-
Import metadata from a CTSimU metadata file or a given metadata dictionary.
Parameters
filename
:str
-
Path to a CTSimU metadata file.
Default value:
None
json_dict
:dict
-
Provide a dictionary with a metadata structure instead of a file.
Default value:
None
import_referenced_scenario
:bool
-
Import the scenario JSON file that's referenced in the metadata file? Generates a warning if this fails.
The scenario definition will be searched at two locations in the following order:
-
Try to read from external file defined by
acquisition_geometry.path_to_CTSimU_JSON
. -
Try to read embedded scenario definition from
simulation.ctsimu_scenario
. Note that external drift files specified in the scenario will likely fail to load. An error will be issued in this case.
Default value:
False
-
Expand source code
def read_metadata(self, filename:str=None, json_dict:dict=None, import_referenced_scenario:bool=False): """Import metadata from a CTSimU metadata file or a given metadata dictionary. Parameters ---------- filename : str Path to a CTSimU metadata file. Default value: `None` json_dict : dict Provide a dictionary with a metadata structure instead of a file. Default value: `None` import_referenced_scenario : bool Import the scenario JSON file that's referenced in the metadata file? Generates a warning if this fails. The scenario definition will be searched at two locations in the following order: 1. Try to read from external file defined by `acquisition_geometry.path_to_CTSimU_JSON`. 2. Try to read embedded scenario definition from `simulation.ctsimu_scenario`. Note that external drift files specified in the scenario will likely fail to load. An error will be issued in this case. Default value: `False` """ if filename is not None: json_dict = read_json_file(filename=filename) self.current_metadata_path = filename self.current_metadata_directory = os.path.dirname(filename) self.current_metadata_file = os.path.basename(filename) self.current_metadata_basename, extension = os.path.splitext(self.current_metadata_file) # If a file is read, we want to make sure that it is a valid # and supported metadata file: if isinstance(json_dict, dict): file_type = get_value(json_dict, ["file", "file_type"]) if file_type != "CTSimU Metadata": raise Exception(f"Invalid metadata structure: the string 'CTSimU Metadata' was not found in 'file.file_type' in the metadata file {file}.") fileformatversion = get_value(json_dict, ["file", "file_format_version"]) if not is_version_supported(ctsimu_supported_metadata_version, fileformatversion): raise Exception(f"Unsupported or invalid metadata version. Currently supported: up to {ctsimu_supported_metadata_version['major']}.{ctsimu_supported_metadata_version['minor']}.") else: raise Exception(f"Error when reading the metadata file: {filename}. read_json_file did not return a Python dictionary.") # If we get a `json_dict` as function parameter, we do not # test for a valid version because reduced/simplified metadata # structures should be supported as well. self.metadata.set_from_json(json_dict) self.metadata_is_set = True if import_referenced_scenario: # Import the scenario that's referenced in the metadata file. ref_file = self.metadata.get(["acquisition_geometry", "path_to_CTSimU_JSON"]) import_success = False try: if (ref_file is not None) and (ref_file != ""): if isinstance(ref_file, str): ref_file = abspath_of_referenced_file(self.current_metadata_path, ref_file) else: raise Exception("read_metadata: path_to_CTSimU_JSON is not a string.") # Try to import scenario: self.read(filename=ref_file) import_success = True except Exception as e: warnings.warn(str(e)) import_success = False if not import_success: # Try to import the embedded scenario structure. if json_exists_and_not_null(json_dict, ["simulation", "ctsimu_scenario"]): self.read(json_dict=json_dict["simulation"]["ctsimu_scenario"]) import_success = True # Create default metadata in case the original # metadata file did not contain all information that's needed: self.create_default_metadata() # Re-import metadata: self.metadata.set_from_json(json_dict)
def reset(self)
-
Reset scenario: delete all drifts, deviations and sample and material information.
Expand source code
def reset(self): """Reset scenario: delete all drifts, deviations and sample and material information.""" for group in self.subgroups: group.reset()
def reset_metadata(self)
-
Reset scenario's metadata information.
Expand source code
def reset_metadata(self): """Reset scenario's metadata information.""" self.current_metadata_path = None self.current_metadata_file = None self.current_metadata_basename = None self.current_metadata_directory = None self.metadata_is_set = False # Create new, empty metadata: self.metadata = Metadata()
def set_frame(self, frame: float = 0, reconstruction: bool = False)
-
Set the current frame representation of the scenario.
Parameters
frame
:float
-
Frame number, starting at zero and usually going to
{n_projections}-1
.Default value:
0
reconstruction
:bool
-
Set the scenario geometry as seen by the reconstruction software? If
True
, this will only obey drifts and deviations that areknown_to_reconstruction
.Default value:
False
Expand source code
def set_frame(self, frame:float=0, reconstruction:bool=False): """Set the current frame representation of the scenario. Parameters ---------- frame : float Frame number, starting at zero and usually going to `{n_projections}-1`. Default value: `0` reconstruction : bool Set the scenario geometry as seen by the reconstruction software? If `True`, this will only obey drifts and deviations that are `known_to_reconstruction`. Default value: `False` """ self.current_frame = frame # Number of frames: n_frames = self.n_frames() stage_deg = self.current_stage_angle() stage_rot = math.radians(stage_deg) # Update materials: for material in self.materials: material.set_frame(frame, n_frames, reconstruction) # Update stage, source, detector and other parameters: self.stage.set_frame(frame, n_frames, stage_rot, None, reconstruction) self.source.set_frame(frame, n_frames, 0, None, reconstruction) self.detector.set_frame(frame, n_frames, 0, None, reconstruction) self.file.set_frame(frame, n_frames, reconstruction) self.environment.set_frame(frame, n_frames, reconstruction) self.acquisition.set_frame(frame, n_frames, reconstruction) # Update samples: stage_cs = self.stage.coordinate_system for sample in self.samples: sample.set_frame(frame, n_frames, 0, stage_cs, reconstruction)
def write(self, filename: str)
-
Write a scenario JSON file.
Parameters
filename
:str
- Filename for the scenario file.
Expand source code
def write(self, filename:str): """Write a scenario JSON file. Parameters ---------- filename : str Filename for the scenario file. """ if filename is not None: self.file.file_format_version.set("major", ctsimu_supported_scenario_version["major"]) self.file.file_format_version.set("minor", ctsimu_supported_scenario_version["minor"]) write_json_file(filename=filename, dictionary=self.json_dict())
def write_CERA_config(self, save_dir: str = None, basename: str = None, create_vgi: bool = False)
-
Write CERA reconstruction config files.
Parameters
save_dir
:str
-
Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match.
If
None
is given, a directory will be inferred:-
If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern:
{json_scenario_basename}/reconstruction
-
If a reconstruction metadata file was imported, the config files will be stored next to the metadata file.
Default value:
None
-
basename
:str
-
Base name for the created files. If
None
is given, the base name will be inferred from the scenario's metadata.Default value:
None
create_vgi
:bool
- Write VGI file for future reconstruction volume?
Expand source code
def write_CERA_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False): """Write CERA reconstruction config files. Parameters ---------- save_dir : str Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match. If `None` is given, a directory will be inferred: - If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern: `{json_scenario_basename}/reconstruction` - If a reconstruction metadata file was imported, the config files will be stored next to the metadata file. Default value: `None` basename : str Base name for the created files. If `None` is given, the base name will be inferred from the scenario's metadata. Default value: `None` create_vgi : bool Write VGI file for future reconstruction volume? """ matrices = [] if basename is None: # Extract base name from metadata metadata_basename = self.metadata.get(["file", "name"]) if metadata_basename is not None: basename = f"{metadata_basename}_recon_cera" else: basename = f"recon_cera" # Projection files n_projections = self.acquisition.get("number_of_projections") projection_file_pattern = self.metadata.get(["output", "projections", "filename"]) projection_datatype = self.metadata.get(["output", "projections", "datatype"]) projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"]) projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"]) projection_filetype = "tiff" if projection_file_pattern.lower().endswith(".raw"): projection_filetype = "raw" # Acquisition start_angle = self.acquisition.get("start_angle") stop_angle = self.acquisition.get("stop_angle") total_angle = stop_angle - start_angle for p in range(n_projections): self.set_frame(frame=p, reconstruction=True) # CERA projection matrix for projection p: m = self.current_geometry().projection_matrix(mode="CERA") matrices.append(m) # Go back to frame zero: self.set_frame(frame=0, reconstruction=True) if create_vgi: vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi") volume_filename = f"{basename}.raw" self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename) create_CERA_config( geo=self.current_geometry(), projection_file_pattern=projection_file_pattern, basename=basename, save_dir=save_dir, n_projections=n_projections, projection_datatype=projection_datatype, projection_filetype=projection_filetype, projection_byteorder=projection_file_byteorder, projection_headersize=projection_headersize, start_angle=0, # do not compensate the scenario start angle in the reconstruction total_angle=total_angle, scan_direction=self.acquisition.get("direction"), voxels_x=self.metadata.output.tomogram.dimensions.x.get(), voxels_y=self.metadata.output.tomogram.dimensions.y.get(), voxels_z=self.metadata.output.tomogram.dimensions.z.get(), voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(), voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(), voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(), i0max=self.metadata.output.get(["projections", "max_intensity"]), output_datatype=convert(cera_converter["datatype"], self.metadata.output.get(["tomogram", "datatype"])), matrices=matrices )
def write_OpenCT_config(self, save_dir: str = None, basename: str = None, create_vgi: bool = False, variant: str = 'free', abspaths: bool = False)
-
Write OpenCT reconstruction config files.
Parameters
save_dir
:str
-
Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match.
If
None
is given, a directory will be inferred:-
If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern:
{json_scenario_basename}/reconstruction
-
If a reconstruction metadata file was imported, the config files will be stored next to the metadata file.
Default value:
None
-
basename
:str
-
Base name for the created config file. If
None
is given, the base name will be inferred from the scenario's metadata.Default value:
None
create_vgi
:bool
- Write VGI file for future reconstruction volume?
variant
:str
-
Which variant of the OpenCT file format will be created: free trajectory or circular trajectory.
Possible values:
"free"
,"circular"
Default value:
"free"
abspaths
:bool
-
Set to
True
if absolute paths should be used in the OpenCT config file.Default value:
False
(relative paths)
Returns
openct_dict
:dict
- Dictionary with the OpenCT JSON structure.
Expand source code
def write_OpenCT_config(self, save_dir:str=None, basename:str=None, create_vgi:bool=False, variant:str='free', abspaths:bool=False): """Write OpenCT reconstruction config files. Parameters ---------- save_dir : str Folder where to place the CERA config files. This is meant to be the same directory where the reconstruction metadata file is located, such that relative paths will match. If `None` is given, a directory will be inferred: - If only a JSON scenario file was imported to set up the scenario, the config files will be stored in a subdirectory next to the JSON scenario file of the following pattern: `{json_scenario_basename}/reconstruction` - If a reconstruction metadata file was imported, the config files will be stored next to the metadata file. Default value: `None` basename : str Base name for the created config file. If `None` is given, the base name will be inferred from the scenario's metadata. Default value: `None` create_vgi : bool Write VGI file for future reconstruction volume? variant : str Which variant of the OpenCT file format will be created: free trajectory or circular trajectory. Possible values: `"free"`, `"circular"` Default value: `"free"` abspaths : bool Set to `True` if absolute paths should be used in the OpenCT config file. Default value: `False` (relative paths) Returns ------- openct_dict : dict Dictionary with the OpenCT JSON structure. """ matrices = [] if basename is None: # Extract base name from metadata metadata_basename = self.metadata.get(["file", "name"]) if metadata_basename is not None: basename = f"{metadata_basename}_recon_openCT" else: basename = f"recon_openCT" # Name of config file: openct_config_filename = join_dir_and_filename(save_dir, f"{basename}.json") def projections_from_pattern(json_dict:dict): n = get_value(json_dict, ["number"]) # number of images pattern = None filedir = None filename = None filelist = list() if n is not None: if n > 0: pattern = get_value(json_dict, ["filename"]) if abspaths is True: pattern = abspath_of_referenced_file(openct_config_filename, pattern) if pattern is not None: filedir, filename = os.path.split(pattern) # Generate list of projection files: if filename is not None: # Auto-generate projection file list. # List of sequentially numbered projection images, # starting at 0000. for p in range(n): if '%' in filename: try: filelist.append(filename % p) except Exception as e: raise Exception(f"Error in sequentially numbered filename pattern. Please give a percentage sign, followed by the number of digits and a 'd' character: 'example_%04d.tif'. You gave: '{filename}'") else: filelist.append(filename) return n, filedir, filename, filelist # Projection files n_projections, projection_filedir, projection_filename, projection_filelist = projections_from_pattern(self.metadata.output.projections.json_dict()) projection_datatype = self.metadata.get(["output", "projections", "datatype"]) projection_file_byteorder = self.metadata.get(["output", "projections", "byteorder"]) projection_headersize = self.metadata.get(["output", "projections", "headersize", "file"]) projection_filetype = "tiff" if projection_filename is not None: if projection_filename.lower().endswith(".raw"): projection_filetype = "raw" # Dark files; only is corrections need to be applied. openct_dark_image = None if not self.metadata.output.projections.dark_field.projections_corrected.get() is True: n_darks, dark_filedir, dark_filename, dark_filelist = projections_from_pattern(self.metadata.output.projections.dark_field.json_dict()) if isinstance(dark_filelist, list): if len(dark_filelist) > 0: openct_dark_image = join_dir_and_filename(dark_filedir, dark_filelist[0]) # Bright files; only is corrections need to be applied. flat_filedir = None flat_filelist = None if not self.metadata.output.projections.flat_field.projections_corrected.get() is True: n_flats, flat_filedir, flat_filename, flat_filelist = projections_from_pattern(self.metadata.output.projections.flat_field.json_dict()) # Acquisition start_angle = self.acquisition.get("start_angle") stop_angle = self.acquisition.get("stop_angle") total_angle = stop_angle - start_angle for p in range(n_projections): self.set_frame(frame=p, reconstruction=True) # CERA projection matrix for projection p: m = self.current_geometry().projection_matrix(mode="OpenCT") matrices.append(m) # Go back to frame zero: self.set_frame(frame=0, reconstruction=True) volume_filename = f"{basename}.img" if create_vgi: vgi_filename = join_dir_and_filename(save_dir, f"{basename}.vgi") self.write_recon_VGI(vgi_filename=vgi_filename, name=basename, volume_filename=volume_filename) openct_dict = create_OpenCT_config( geo=self.current_geometry(), filename=openct_config_filename, variant=variant, projection_files=projection_filelist, projection_dir=projection_filedir, projection_datatype=projection_datatype, projection_filetype=projection_filetype, projection_headersize=projection_headersize, projection_byteorder=projection_file_byteorder, total_angle=total_angle, scan_direction=self.acquisition.get("direction"), matrices=matrices, volumename=volume_filename, voxels_x=self.metadata.output.tomogram.dimensions.x.get(), voxels_y=self.metadata.output.tomogram.dimensions.y.get(), voxels_z=self.metadata.output.tomogram.dimensions.z.get(), voxelsize_x=self.metadata.output.tomogram.voxelsize.x.get(), voxelsize_y=self.metadata.output.tomogram.voxelsize.y.get(), voxelsize_z=self.metadata.output.tomogram.voxelsize.z.get(), bright_image_dir=flat_filedir, bright_images=flat_filelist, dark_image=openct_dark_image) return openct_dict
def write_metadata(self, filename: str)
-
Write a metadata JSON file for the scenario.
Parameters
filename
:str
- Filename for the metadata file.
Expand source code
def write_metadata(self, filename:str): """Write a metadata JSON file for the scenario. Parameters ---------- filename : str Filename for the metadata file. """ if filename is not None: metadata_dict = self.metadata.json_dict() # potentially add simulation.ctsimu_scenario here: metadata_dict["simulation"]["ctsimu_scenario"] = self.json_dict() write_json_file(filename=filename, dictionary=metadata_dict)
def write_recon_VGI(self, name: str = '', volume_filename: str = '', vgi_filename: str = None)
-
Write a VGI file for the reconstruction volume such that it can be loaded with VGSTUDIO.
Expand source code
def write_recon_VGI(self, name:str="", volume_filename:str="", vgi_filename:str=None): """Write a VGI file for the reconstruction volume such that it can be loaded with VGSTUDIO.""" voxels_x = self.metadata.output.get(["tomogram", "dimensions", "x"]) voxels_y = self.metadata.output.get(["tomogram", "dimensions", "y"]) voxels_z = self.metadata.output.get(["tomogram", "dimensions", "z"]) voxelsize_x = self.metadata.output.get(["tomogram", "voxelsize", "x"]) voxelsize_y = self.metadata.output.get(["tomogram", "voxelsize", "y"]) voxelsize_z = self.metadata.output.get(["tomogram", "voxelsize", "z"]) output_datatype = self.metadata.output.get(["tomogram", "datatype"]) if output_datatype == "uint16": dataTypeOutput = "unsigned integer" bits = 16 datarangelower = 0 datarangeupper = -1 else: dataTypeOutput = "float" bits = 32 datarangelower = -1 datarangeupper = 1 vgi_content = f"""{{volume1}} [representation] size = {voxels_x} {voxels_y} {voxels_z} datatype = {dataTypeOutput} datarange = {datarangelower} {datarangeupper} bitsperelement = {bits} [file1] SkipHeader = 0 FileFormat = raw Size = {voxels_x} {voxels_y} {voxels_z} Name = {volume_filename} Datatype = {dataTypeOutput} datarange = {datarangelower} {datarangeupper} BitsPerElement = {bits} {{volumeprimitive12}} [geometry] resolution = {voxelsize_x} {voxelsize_y} {voxelsize_z} unit = mm [volume] volume = volume1 [description] text = {name}""" if vgi_filename is not None: touch_directory(vgi_filename) with open(vgi_filename, 'w') as f: f.write(vgi_content) f.close() return vgi_content