Source code for pylinac.core.image_generator.simulators
from __future__ import annotations
from abc import ABC
import numpy as np
from plotly import graph_objects as go
from pydicom.dataset import Dataset, FileMetaDataset
from pydicom.uid import UID
from ..array_utils import array_to_dicom
from ..plotly_utils import add_title
from .layers import Layer
def generate_file_metadata() -> Dataset:
file_meta = FileMetaDataset()
file_meta.TransferSyntaxUID = UID(
"1.2.840.10008.1.2"
) # default DICOM transfer syntax
return file_meta
class Simulator(ABC):
"""Abstract class for an image simulator"""
pixel_size: float
shape: (int, int)
image: np.ndarray
def __init__(self, sid: float = 1500):
"""
Parameters
----------
sid
Source to image distance in mm.
"""
self.image = np.zeros(self.shape, np.uint16)
self.sid = sid
self.mag_factor = sid / 1000
def add_layer(self, layer: Layer) -> None:
"""Add a layer to the image"""
self.image = layer.apply(self.image, self.pixel_size, self.mag_factor)
def as_dicom(
self,
gantry_angle: float = 0.0,
coll_angle: float = 0.0,
table_angle: float = 0.0,
invert_array: bool = False,
tags: dict | None = None,
) -> Dataset:
"""Create and return a pydicom Dataset. I.e. create a pseudo-DICOM image."""
if invert_array:
array = -self.image + self.image.max() + self.image.min()
else:
array = self.image
return array_to_dicom(
array=array,
sid=self.sid,
gantry=gantry_angle,
coll=coll_angle,
couch=table_angle,
dpi=25.4 / self.pixel_size,
extra_tags=tags or {},
)
def generate_dicom(self, file_out_name: str, *args, **kwargs) -> None:
"""Save the simulated image to a DICOM file.
See Also
--------
as_dicom
"""
ds = self.as_dicom(*args, **kwargs)
ds.save_as(file_out_name, write_like_original=False)
def plot(self, show: bool = True) -> go.Figure:
"""Plot the simulated image."""
fig = go.Figure()
fig.add_heatmap(
z=self.image,
colorscale="gray",
x0=-self.image.shape[1] / 2 * self.pixel_size,
dx=self.pixel_size,
y0=-self.image.shape[0] / 2 * self.pixel_size,
dy=self.pixel_size,
)
fig.update_layout(
yaxis_constrain="domain",
xaxis_scaleanchor="y",
xaxis_constrain="domain",
xaxis_title="Crossplane (mm)",
yaxis_title="Inplane (mm)",
)
add_title(fig, f"Simulated {self.__class__.__name__} @{self.sid}mm SID")
if show:
fig.show()
return fig
[docs]
class AS500Image(Simulator):
"""Simulates an AS500 EPID image."""
pixel_size: float = 0.78125
shape: (int, int) = (384, 512)
[docs]
class AS1000Image(Simulator):
"""Simulates an AS1000 EPID image."""
pixel_size: float = 0.390625
shape: (int, int) = (768, 1024)
[docs]
class AS1200Image(Simulator):
"""Simulates an AS1200 EPID image."""
pixel_size: float = 0.336
shape: (int, int) = (1280, 1280)