Skip to content

Commit c65525f

Browse files
committed
Doc sections on volumes and images
1 parent dce2644 commit c65525f

7 files changed

Lines changed: 1263 additions & 246 deletions

File tree

docs/general.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,7 @@ parts of the library.
1010
:maxdepth: 2
1111
:caption: Contents:
1212

13+
image
14+
pixel_transforms
15+
volume
1316
coding

docs/image.rst

Lines changed: 258 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
.. _image:
2+
3+
Images
4+
======
5+
6+
Highdicom's :class:`highdicom.Image` class is a fundamental class that provides
7+
methods for working with existing DICOM images. It inherits from pydicom's
8+
``pydicom.Dataset`` class, and therefore you can access individual DICOM
9+
attributes just like you can for any dataset. However, the
10+
:class:`highdicom.Image` class also provides further functionality to make it
11+
easier to access frames with pixel transforms applied and arrange frames based
12+
on metadata attributes.
13+
14+
Most of highdicom's classes correspond to individual Information Object
15+
Definitions defined within the DICOM standard, for example `Segmentation
16+
Image`, `Parametric Map`, or `Comprehensive3DSR`. However this is **not** the
17+
case for the :class:`highdicom.Image` class, which captures behavior common to
18+
a large number of different IODs. Any IOD that includes pixel data can be
19+
loaded into an Image object. This includes both single frame ("legacy") and
20+
newer multi-frame objects.
21+
22+
Reading Images
23+
--------------
24+
25+
You can read in an image from a file using the :func:`highdicom.imread()`
26+
function:
27+
28+
.. code-block:: python
29+
30+
import highdicom as hd
31+
32+
# This is a test file in the highdicom git repository
33+
im = hd.imread('data/test_files/ct_image.dcm')
34+
35+
Alternatively, you can convert an existing ``pydicom.Dataset`` instance that
36+
represents an image to a :class:`highdicom.Image` instances using the
37+
:meth:`highdicom.Image.from_dataset()` method.
38+
39+
.. code-block:: python
40+
41+
import pydicom
42+
import highdicom as hd
43+
44+
# This is a test file in the highdicom git repository
45+
dcm = pydicom.dcmread('data/test_files/ct_image.dcm')
46+
47+
im = hd.Image.from_dataset(dcm)
48+
49+
:class:`highdicom.Image` instances cannot be created directly using a
50+
constructor, they must always be created from an existing DICOM object.
51+
52+
Accessing Frames
53+
----------------
54+
55+
The :class:`highdicom.Image` class has three methods for accessing individual
56+
frames of the image:
57+
58+
* :meth:`highdicom.Image.get_raw_frame()`: This returns the raw bytes
59+
containing the information for a single frame as a Python ``bytes`` object.
60+
If the image uses a compressed transfer syntax (such as JPEG or its
61+
variants), the compressed bytestream is returned. This method is intended for
62+
advanced users and use cases.
63+
* :meth:`highdicom.Image.get_stored_frame()`: This returns the frame as a NumPy
64+
array with minimal processing. The raw bytes are decompressed if necessary
65+
and reshaped to form the frame of the correct shape, but no further pixel
66+
transforms are applied. These are referred to as "stored values" within the
67+
DICOM standard. Note that the pydicom `.pixel_array` property returns stored
68+
values for all frames at once.
69+
* :meth:`highdicom.Image.get_frame()`: In addition to the above, this method
70+
applies pixel transforms stored in the file to the stored values before
71+
returning them. The transforms applied are configurable through parameters
72+
(see :doc:`pixel_transforms` for more details on pixel transforms), but by
73+
default any pixel transform found in the dataset except the value-of-interest
74+
(VOI) transform is applied. This should be your default way of accessing
75+
image frames in most cases, since it will typtically return the pixels as the
76+
creator of the object intended them to be understood. By default, the
77+
returned frames have datatype `numpy.float64`, but this can be controlled
78+
using the `dtype` parameter.
79+
80+
For all methods, the first parameter ``frame_number`` is an integer giving the
81+
number of the frame, where the first frame has index 1. This one-based indexing
82+
may be unnatural for Python programming (which generally uses 0-based
83+
indexing). The reason for this choice is that the DICOM standard numbers frames
84+
starting at 1, and in particular if a DICOM object contains references to its
85+
frames, or those of other objects, 1-based frame numbers are used. If you
86+
prefer to use 0-based indexing, you can specify ``as_index=True``.
87+
88+
.. code-block:: python
89+
90+
import numpy as np
91+
import highdicom as hd
92+
93+
94+
# This is a test file in the highdicom git repository
95+
im = hd.imread('data/test_files/ct_image.dcm')
96+
97+
# Get raw bytes for the first frame
98+
first_frame = im.get_raw_frame(1)
99+
print(type(first_frame))
100+
# <class 'bytes'>
101+
102+
# Get stored values for the first frame
103+
first_frame = im.get_stored_frame(1)
104+
print(first_frame.min(), first_frame.max())
105+
# 128 2191
106+
107+
# Get pixels after rescale/slope applied
108+
first_frame = im.get_frame(1)
109+
print(first_frame.dtype)
110+
# float64
111+
print(first_frame.min(), first_frame.max())
112+
# -896.0 1167.0
113+
114+
# Specify an integer datatype
115+
first_frame = im.get_frame(1, dtype=np.int32)
116+
print(first_frame.dtype)
117+
# int32
118+
119+
# Alternative, using 0-based index
120+
first_frame = im.get_frame(0, as_index=True)
121+
122+
These three methods process the raw pixel data "lazily" as needed to avoid
123+
processing unecessary frames. If you know that you are likely to access frames
124+
multiple times, you can force caching of the stored values by accessing the
125+
``.pixel_array`` property (inherited from ``pydicom.Dataset``).
126+
127+
Accessing Total Pixel Matrices
128+
------------------------------
129+
130+
Digital pathology images in DICOM format are typically stored as "tiled"
131+
images, where frames are arranged in a 2D pattern across a plane to form a
132+
large "total pixel matrix". For such images, you typically want to work with
133+
the large 2D total pixel matrix that is formed by correctly arranging the tiles
134+
into a 2D array rather than 3D arrays of stacked frames. `highdicom` provides
135+
the :meth:`highdicom.Image.get_total_pixel_matrix()` method for this purpose.
136+
137+
Called without any parameters, it returns a 2D array containing the full total
138+
pixel matrix. The two dimensions are the spatial dimensions. Behind the scenes
139+
highdicom has stitched together the required frames stored in the original file
140+
for you.
141+
142+
.. code-block:: python
143+
144+
import highdicom as hd
145+
146+
# Read in a tiled test file from the highdicom repo
147+
im = hd.imread('data/test_files/sm_image.dcm')
148+
149+
# Get the full total pixel matrix
150+
tpm = im.get_total_pixel_matrix()
151+
152+
expected_shape = (
153+
im.TotalPixelMatrixRows,
154+
im.TotalPixelMatrixColumns,
155+
3, # RGB channels
156+
)
157+
assert tpm.shape == expected_shape
158+
159+
Furthermore, you can request a sub-region of the full total pixel matrix by
160+
specifying the start and/or stop indices for the rows and/or columns within the
161+
total pixel matrix. Note that this method follows DICOM 1-based convention for
162+
indexing rows and columns, i.e. the first row and column of the total pixel
163+
matrix are indexed by the number 1 (not 0 as is common within Python). Negative
164+
indices are also supported to index relative to the last row or column, with -1
165+
being the index of the last row or column. Like for standard Python indexing,
166+
the stop indices are specified as one beyond the final row/column in the
167+
returned array. The requested region does not have to start or stop
168+
at the edges of the underlying frames: `highdicom` stitches together only the
169+
relevant parts of the frames to create the requested image for you.
170+
171+
.. code-block:: python
172+
173+
import highdicom as hd
174+
175+
# Read in a tiled test file from the highdicom repo
176+
im = hd.imread('data/test_files/sm_image.dcm')
177+
178+
# Get a region of the total pixel matrix
179+
tpm = im.get_total_pixel_matrix(
180+
row_start=15,
181+
row_end=25,
182+
column_start=26,
183+
)
184+
185+
expected_shape = (10, 25, 3)
186+
assert tpm.shape == expected_shape
187+
188+
Accessing Volumes
189+
-----------------
190+
191+
Many multi-frame images, especially from radiology modalities such as CT, MRI,
192+
DBT, and PET, contain frames that can be arranged together to form voxels on a
193+
regularly-sampled rectangular 3D grid. The :meth:`highdicom.Image.get_volume()`
194+
method checks for this case and, if possible, returns a 3D voxel array array
195+
with the affine matrix describing its position in the frame of reference
196+
coordinate system, as a :class:`highdicom.Volume`. To just check whether it is
197+
possible to form a volume from the frames, use the
198+
:class:`highdicim.Image.get_volume_geometry()` method, which will return
199+
``None`` if no volume can be formed.
200+
201+
.. code-block:: python
202+
203+
from pydicom.data import get_testdata_file
204+
205+
import highdicom as hd
206+
207+
# Load an enhanced (multiframe) CT image
208+
im = hd.imread(get_testdata_file('eCT_Supplemental.dcm'))
209+
210+
geometry = im.get_volume_geometry()
211+
212+
assert geometry is not None
213+
214+
vol = im.get_volume()
215+
print(vol.spatial_shape)
216+
# (2, 512, 512)
217+
218+
print(vol.affine)
219+
# [[ 0. 0. -0.388672 99.5 ]
220+
# [ -0. 0.388672 0. -301.5 ]
221+
# [ 10. 0. 0. -159. ]
222+
# [ 0. 0. 0. 1. ]]
223+
224+
Further parameters allow you to access a sub-region of the volume and control
225+
the pixel transforms applied to the frames.
226+
227+
Any single frame image that defines its position within the frame-of-reference
228+
coordinate system can accessed as a volume, as can any image with a total pixel
229+
matrix. In these cases, the first spatial dimension will always have shape 1.
230+
231+
See :doc:`volume` for an overview of the :class:`highdicom.Volume` class.
232+
233+
Lazy Frame Retrieval
234+
--------------------
235+
236+
The :func:`highdicom.imread()` function provides the ``lazy_frame_retrieval``
237+
parameter. If used, the metadata is loaded from the file without the pixel
238+
data. Pixel data is subsequently loaded from the file whenever it is needed by
239+
one of the :class:`highdicom.Image` object's methods. This can save loaded
240+
unneeded pixel data from file when only a subset of it is needed.
241+
242+
In this example, lazy frame retrieval is used to avoid loading all frames of a
243+
tiled image:
244+
245+
.. code-block:: python
246+
247+
import highdicom as hd
248+
249+
# Read in a tiled test file from the highdicom repo
250+
im = hd.imread(
251+
'data/test_files/sm_image.dcm',
252+
lazy_frame_retrieval=True
253+
)
254+
255+
# Get a region of the total pixel matrix
256+
tpm = im.get_total_pixel_matrix(row_end=20)
257+
258+
Whether this saves time depends on your usage patterns and hardware.

docs/pixel_transforms.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.. _pixel-transforms:
2+
3+
Pixel Transforms
4+
================

0 commit comments

Comments
 (0)