Images
Highdicom’s highdicom.Image class is a fundamental class that provides
methods for working with existing DICOM images. It inherits from pydicom’s
pydicom.Dataset class, and therefore you can access individual DICOM
attributes just like you can for any dataset. However, the
highdicom.Image class also provides further functionality to make it
easier to access frames with pixel transforms applied and arrange frames based
on metadata attributes.
Most of highdicom’s classes correspond to individual Information Object
Definitions defined within the DICOM standard, for example Segmentation
Image, Parametric Map, or Comprehensive3DSR. However this is not the
case for the highdicom.Image class, which captures behavior common to
a large number of different IODs. Any IOD that includes pixel data can be
loaded into an Image object. This includes both single frame (“legacy”) and
newer multi-frame objects.
Reading Images
You can read in an image from a file using the highdicom.imread()
function:
import highdicom as hd
# This is a test file in the highdicom git repository
im = hd.imread('data/test_files/ct_image.dcm')
Alternatively, you can convert an existing pydicom.Dataset instance that
represents an image to a highdicom.Image instances using the
highdicom.Image.from_dataset() method.
import pydicom
import highdicom as hd
# This is a test file in the highdicom git repository
dcm = pydicom.dcmread('data/test_files/ct_image.dcm')
im = hd.Image.from_dataset(dcm)
highdicom.Image instances cannot be created directly using a
constructor, they must always be created from an existing DICOM object.
Accessing Frames
The highdicom.Image class has three methods for accessing individual
frames of the image:
highdicom.Image.get_raw_frame(): This returns the raw bytes containing the information for a single frame as a Pythonbytesobject. If the image uses a compressed transfer syntax (such as JPEG or its variants), the compressed bytestream is returned. This method is intended for advanced users and use cases.highdicom.Image.get_stored_frame(): This returns the frame as a NumPy array with minimal processing. The raw bytes are decompressed if necessary and reshaped to form the frame of the correct shape, but no further pixel transforms are applied. These are referred to as “stored values” within the DICOM standard. Note that the pydicom.pixel_arrayproperty returns stored values for all frames at once.highdicom.Image.get_frame(): In addition to the above, this method applies pixel transforms stored in the file to the stored values before returning them. The transforms applied are configurable through parameters (see Pixel Transforms for more details on pixel transforms), but by default any pixel transform found in the dataset except the value-of-interest (VOI) transform is applied. This should be your default way of accessing image frames in most cases, since it will typtically return the pixels as the creator of the object intended them to be understood. By default, the returned frames have datatypenumpy.float64, but this can be controlled using thedtypeparameter.
For all methods, the first parameter frame_number is an integer giving the
number of the frame, where the first frame has index 1. This one-based indexing
may be unnatural for Python programming (which generally uses 0-based
indexing). The reason for this choice is that the DICOM standard numbers frames
starting at 1, and in particular if a DICOM object contains references to its
frames, or those of other objects, 1-based frame numbers are used. If you
prefer to use 0-based indexing, you can specify as_index=True.
import numpy as np
import highdicom as hd
# This is a test file in the highdicom git repository
im = hd.imread('data/test_files/ct_image.dcm')
# Get raw bytes for the first frame
first_frame = im.get_raw_frame(1)
print(type(first_frame))
# <class 'bytes'>
# Get stored values for the first frame
first_frame = im.get_stored_frame(1)
print(first_frame.min(), first_frame.max())
# 128 2191
# Get pixels after rescale/slope applied
first_frame = im.get_frame(1)
print(first_frame.dtype)
# float64
print(first_frame.min(), first_frame.max())
# -896.0 1167.0
# Specify an integer datatype
first_frame = im.get_frame(1, dtype=np.int32)
print(first_frame.dtype)
# int32
# Alternative, using 0-based index
first_frame = im.get_frame(0, as_index=True)
These three methods process the raw pixel data “lazily” as needed to avoid
processing unnecessary frames. If you know that you are likely to access frames
multiple times, you can force caching of the stored values by accessing the
.pixel_array property (inherited from pydicom.Dataset).
Additionally, there are two methods for accessing multiple frames at a time:
highdicom.Image.get_stored_frames(): Returns a stack of multiple stored frames. The first parameter is a list (or other iterable) of frame numbers. If omitted, all frames are returned in the order they are stored in the image.highdicom.Image.get_frames(): Returns a stack of multiple frames with pixel transforms applied. The first parameter is a list (or other iterable) of frame numbers. If omitted, all frames are returned in the order they are stored in the image.
Accessing Total Pixel Matrices
Digital pathology images in DICOM format are typically stored as “tiled”
images, where frames are arranged in a 2D pattern across a plane to form a
large “total pixel matrix”. For such images, you typically want to work with
the large 2D total pixel matrix that is formed by correctly arranging the tiles
into a 2D array rather than the 3D arrays of stacked frames that are returned
by pydicom by default. highdicom provides the
highdicom.Image.get_total_pixel_matrix() method for this purpose.
Called without any parameters, it returns a 2D array containing the full total pixel matrix. The two dimensions are the spatial dimensions. Behind the scenes highdicom has stitched together the required frames stored in the original file for you.
import highdicom as hd
# Read in a tiled test file from the highdicom repo
im = hd.imread('data/test_files/sm_image.dcm')
# Get the full total pixel matrix
tpm = im.get_total_pixel_matrix()
expected_shape = (
im.TotalPixelMatrixRows,
im.TotalPixelMatrixColumns,
3, # RGB channels
)
assert tpm.shape == expected_shape
Furthermore, you can request a sub-region of the full total pixel matrix by
specifying the start and/or stop indices for the rows and/or columns within the
total pixel matrix. Note that this method follows DICOM 1-based convention for
indexing rows and columns, i.e. the first row and column of the total pixel
matrix are indexed by the number 1 (not 0 as is common within Python). Negative
indices are also supported to index relative to the last row or column, with -1
being the index of the last row or column. Like for standard Python indexing,
the stop indices are specified as one beyond the final row/column in the
returned array. You can alternatively use 0-based indices by specifying the
as_indices parameter as True. The requested region does not have to start
or stop at the edges of the underlying frames: highdicom stitches together
only the relevant parts of the frames to create the requested image for you.
import highdicom as hd
# Read in a tiled test file from the highdicom repo
im = hd.imread('data/test_files/sm_image.dcm')
# Get a region of the total pixel matrix
tpm = im.get_total_pixel_matrix(
row_start=15,
row_end=25,
column_start=26,
)
expected_shape = (10, 25, 3)
assert tpm.shape == expected_shape
Accessing Volumes
Many multi-frame images, especially from radiology modalities such as CT, MRI,
DBT, and PET, contain frames that can be arranged together to form voxels on a
regularly-sampled rectangular 3D grid. The highdicom.Image.get_volume()
method checks for this case and, if possible, returns a 3D voxel array
with the affine matrix describing its position in the frame of reference
coordinate system, as a highdicom.Volume. To just check whether it is
possible to form a volume from the frames, use the
highdicim.Image.get_volume_geometry() method, which will return
None if no volume can be formed.
from pydicom.data import get_testdata_file
import highdicom as hd
# Load an enhanced (multiframe) CT image
im = hd.imread(get_testdata_file('eCT_Supplemental.dcm'))
geometry = im.get_volume_geometry()
assert geometry is not None
vol = im.get_volume()
print(vol.spatial_shape)
# (2, 512, 512)
print(vol.affine)
# [[ 0. 0. -0.388672 99.5 ]
# [ -0. 0.388672 0. -301.5 ]
# [ 10. 0. 0. -159. ]
# [ 0. 0. 0. 1. ]]
Further parameters allow you to access a sub-region of the volume and control the pixel transforms applied to the frames.
Any single frame image that defines its position within the frame-of-reference coordinate system can accessed as a volume, as can any image with a total pixel matrix. In these cases, the first spatial dimension will always have shape 1.
See Volumes for an overview of the highdicom.Volume class.
Lazy Frame Retrieval
The highdicom.imread() function provides the lazy_frame_retrieval
parameter. If used, the metadata is loaded from the file without the pixel
data. Pixel data is subsequently loaded from the file whenever it is needed by
one of the highdicom.Image object’s methods. This can avoid loading
unneeded pixel data from file when only a subset of it is needed.
In this example, lazy frame retrieval is used to avoid loading all frames of a tiled image:
import highdicom as hd
# Read in a tiled test file from the highdicom repo
im = hd.imread(
'data/test_files/sm_image.dcm',
lazy_frame_retrieval=True
)
# Get a region of the total pixel matrix
tpm = im.get_total_pixel_matrix(row_end=20)
Whether this saves time depends on your usage patterns and hardware. It can be particularly effective when reading from remote filesystems and cloud storage (see Reading from Remote Filesystems). Furthermore in certain situations highdicom needs to parse the entire pixel data element in order to determine frame boundaries. This occurs when the frames are compressed using an encapsulated transfer syntax but there is no offset table giving the locations of frame boundaries within the file. An offset table can take the form of either a basic offset table (BOT) at the start of the PixelData element or an extended offset table (EOT) as a separate attribute in the metadata. These offset tables are not required, but often one of them is included in images. Without an offset table, the potential speed benefits of using lazy frame retrieval are usually eliminated, even if only a small number of frames are loaded.