|
| 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. |
0 commit comments