File: ImageReader.md

package info (click to toggle)
vtk-dicom 0.8.17-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 7,032 kB
  • sloc: cpp: 113,806; python: 2,041; makefile: 43; tcl: 10
file content (283 lines) | stat: -rw-r--r-- 13,187 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
## Image Reader {#imageReader}

@brief Nitty-gritty details for loading DICOM images.

## Overview

The vtkDICOMReader converts a series of images (usually collated by
vtkDICOMDirectory) into three outputs:
1. a vtkImageData object to hold the pixel data for the volume
2. a vtkMatrix4x4 to hold the position and orientation of the volume
3. a vtkDICOMMetaData object to hold the meta-data

Each slice of the output image data is generated from one of the input
files (or, in the case of multi-frame images, from one frame).  The
slices are sorted into the correct order by the reader, so it is not
necessary to sort the files beforehand.  The reader also computes a 4x4
matrix, the "PatientMatrix", that can be used to convert from image data
coordinates (measured in millimeters from the center of the first
voxel), to DICOM patient coordinates (as described in the
[DICOM Image Plane Module](http://dicom.nema.org/medical/dicom/current/output/chtml/part03/sect_C.7.6.2.html)).

In order to achieve this, the reader checks the Image Position and Image
Orientation that are recorded in the meta data for each slice.  Then, if
and only if the slices have the same orientation and the slice positions
fit a line, the reader sorts the slices according to location.  The
vtkImageData *z* spacing is then set to the average center-to-center
distance between adjacent slices.

In the absence of Image Position information in the meta data, or if the
slices do not form a rectilinear or parallelepiped volume, then the
slices are sorted only according to the Instance Number in the meta
data.  There is also a reader method called SortingOff() than can be
called to disable sorting entirely, so that the order of the slices in
the vtkImageData will reflect the order of the list of files provided to
the reader.

## Row order

DICOM images are rasterized from top to bottom, meaning that the first
pixel in the file is meant to be displayed at the top left corner of the
viewport.  The convention for the VTK image reader classes, however, is
to store the pixels in memory such that the first pixel is at the bottom
left corner.  To support his convention, the default behavior of the
vtkDICOMReader is to flip the images while reading them so that last
image row in the file becomes the first row of the vtkImageData.  In
this way, if the image is displayed by the vtkImageViewer, it will be
displayed the right way up.

However, flipping the image has consequences.  When the reader flips the
image image, it likewise flips the PatientMatrix with respect to the
ImageOrientationPatient stored in the file.  Also, since a flip along
one axis requires a flip along another axis in order for the coordinate
system to obey the right-hand-rule, the reversal of the ordering of the
rows is accompanied by a reversal of the ordering of the slices.  This
is done because the alternative, introducing a PatientMatrix with a
negative determinant, will cause difficulties with 3D processing and
display of the images.

Hence, unless the intent is to use vtkDICOMReader in a simple 2D display
pipeline with vtkImageViewer, it is recommended to call the
SetMemoryRowOrderToFileNative() method when reading the images.  This
will keep the keep the image in its original top-to-bottom ordering.

~~~~~~~~{.cpp}
  vtkNew<vtkDICOMReader> reader;
  reader->SetFileNames(fileNameArray);
  reader->SetMemoryRowOrderToFileNative();
  reader->Update();

  // get the matrix to use when displaying the data
  // (this matrix provides position and orientation)
  vtkMatrix4x4 *matrix = reader->GetPatientMatrix();
~~~~~~~~

## CT gantry tilt

Certain CT scan protocols present a challenge for 3D image analysists
because the slice orientation is tilted with respect to the scan
direction, resulting in an acquisition volume that is a parallelepiped.
This occurs because of the geometry of the CT scanner itself:
traditionally, CT scanners were designed to acquire one slice at a
time with the bed moving a few millimeters between slices.  The CT gantry,
and hence the slices, could be tilted to better align with anatomy,
but tilting the bed of the scanner to match was undesirable for obvious
reasons.  Hence the direction in which the bed moved was not at a right
angle to the slices.

Volume rendering techniques and many 3D filtering techniques assume
a uniform, orthonormal sampling of the data.  It is therefore necessary
to "un-tilt" or rectify these gantry-tilted CT images.  This can be done
with the vtkDICOMCTRectifier class, which rectifies tilted CT volumes
and acts as a simple pass-through for volumes that are already rectilinear.
The rectification is achieved through a simple in-plane translation and
sinc interpolation of the slices.  The resulting slices have the same
orientation as the original slices.

~~~~~~~~{.cpp}
  vtkNew<vtkDICOMCTRectifier> rectify;
  rectify->SetVolumeMatrix(reader->GetPatientMatrix());
  rectify->SetInputConnection(reader->GetOutputPort());
  rectify->Update();

  // get the new PatientMatrix for the rectified volume
  vtkMatrix4x4 *matrix = rectify->GetRectifiedMatrix();
~~~~~~~~

## CT and PET rescaled values

Another challenge with CT images is that the pixel intensity scaling,
as recorded in the RescaleSlope and RescaleIntercept attributes of
the file, can vary from slice to slice.  This is done to make the best
use of the limited dynamic range of the analog-to-digital converter in
the scanner.  By default, the vtkDICOMReader will automatically detect
the changes in RescaleSlope and RescaleIntercept, and will then adjust
the slices so that they are all the same.  This is controlled with the
AutoRescaleOn()/Off() method of the reader.

In general, this automatic adjustment is safe for CT images, because
usually it is only RescaleIntercept that varies, and it usually varies
by a whole number.  There is no loss of fidelity when the integer pixel
value is adjusted by a whole number, as long as it remains within the
range that can be represented by the datatype.  For PET images, however,
the RescaleSlope also varies and it is necessary to use floating-point
values.

If accurate pixel values are required (which is the case in most medical
applications), it is recommended that AutoRescaleOff() is used.  The
rescaling of the pixel values to "real" values should be done with the
vtkDICOMApplyRescale filter, which will produce pixel values as "double"
(or, optionally, as "float") after applying the RescaleSlope and the
RescaleIntercept for each slice.

~~~~~~~~{.cpp}
  vtkNew<vtkDICOMCTReader> reader;
  reader->SetFileNames(fileNameArray);
  reader->SetMemoryRowOrderToFileNative();
  reader->AutoRescaleOff();

  vtkNew<vtkDICOMApplyRescale> rescale;
  rescale->SetInputConnection(reader->GetOutputPort());
  rescale->SetOutputScalarTypeToFloat();
  rescale->Update();
~~~~~~~~

The vtkDICOMReader provides the rescaling information, along with all the
other meta data, via the VTK data pipeline (i.e. via GetOutputPort()).
If you use this filter together with vtkDICOMCTRectifier in the same
pipeline, it is recommended that this filter comes before the rectifier.

## Multi-dimensional images

In addition to sorting slices by location, the reader attempts to detect
multi-dimensional data sets.  It recognizes up to 5 dimensions: *x*, *y*,
*z*, *t*, and a vector dimension.  This is best illustrated by example.
If an MR raw-data DICOM series provides real and imaginary pixel data
at each slice location, then the vtkImageData produced by the reader
will have two components (real and imaginary).  We interpret this
as an image with a vector dimension of 2.

When a time dimension is present, things become interesting.  The default
behavior of the reader is to store adjacent time points in adjacent
vtkImageData slices.  This works well when the images are to be displayed
slice-by-slice.  It is, however, inappropriate if the vtkImageData is to
be displayed as a multi-planar reformat or as a volume.  For this reason,
the vtkDICOMReader has a method called
TimeAsVectorOn() that will cause the reader to treat each voxel as a
time vector.  In other words, if the DICOM data has 10 individual time
slots, then the vtkImageData will have 10 components per voxel (or 30
components in the case of RGB data).  By selecting a specific component
or range of components when displaying the data, one can display a
specific point in time.

Five dimensions come into play when the DICOM series has frames that
are at the same location and within the same time slot.  Going back to
the (real,imaginary) example, if such a series of images is read after
TimeAsVectorOn() is called, then the vtkImageData will have 20 components
per voxel if there are 10 time slots.  The 20 components can be thought of
as 10 component blocks with 2 components per block.  A filter like
vtkImageExtractComponents can be used to extract a block of components
that corresponds to a particular time slot.

If the behavior described in the preceding paragraphs is not desirable,
then one can use the SetDesiredTimeIndex(int) method to read just one
time slot, and use a set of *N* readers to read the *N* time
slots as *N* separate VTK data sets.

~~~~~~~~{.cpp}
  vtkNew<vtkDICOMReader> reader;
  reader->SetFileNames(filenames);

  // read just the meta data, to get the time dimension
  reader->UpdateInformation();

  int numberOfTimeSlots = reader->GetTimeDimension();
  if (numberOfTimeSlots > 1)
  {
    // example: read only the final time slot
    reader->SetDesiredTimeIndex(numberOfTimeSlots-1);
  }

  // update the reader
  reader->Update();
~~~~~~~~

## Enhanced multi-frame and multi-stack files

The DICOM standard allows for multiple slices (frames) per file, or even
multiple stacks of slices per file.  In the case of multi-frame files,
each frame is assigned a position and a time slot and the frames are sorted
according to the slice sorting method described in the previous section.

In multi-stack files there are, as one might expect, more than one
rectilinear (or perhaps non-rectilinear) volume.  If sorting has been
turned off with the SortingOff() method, then all the frames in the file
are read sequentially into vtkImageData slices.  If sorting is on, however,
then the reader is only able to read one stack at a time.  The method
SetDesiredStackID() allows one of the stacks to be chosen by name.

~~~~~~~~{.cpp}
// for this example, 'filename' is multi-frame, multi-stack file
vtkNew<vtkDICOMReader> reader;
reader->SetFileName(filename);

// read the meta data, get a list of stacks
reader->UpdateInformation();
vtkStringArray *stackNames = reader->GetStackIDs();

// specify a stack, here we assume we know the name:
reader->SetDesiredStackID("1");
~~~~~~~~

## Stacks in legacy files

Even though the DICOM standard only describes the use of stacks in
reference to enhanced multi-frame files, the concept is also useful
when working with "legacy" files that only have one slice per file
(such legacy files are, in fact, still the norm while the enhanced
files described in the previous section are relatively uncommon).

The reader defines a stack as a set of slices that have the same
orientation, and whose corners are placed along straight lines in
space.  This allows for rectilinear and parallelepiped volumes,
with the latter being necessary for the tilted-gantry CT scans
discussed previously.  If multiple stacks are required to capture
all of the slices in series, the reader will automatically sort the
slices into stacks and reader->GetStackIDs() will return an array
with multiple values.  In this case, the reader will name the stacks
"0", "1", "2", etcetera.

Generally, such multi-stack series only occur for locators or for
scout series.  For example, a MR scout series will often have a few
images for each orientation, and will therefore result in three
stacks.  A CT locator will be identified as a single-slice stack
that occurs before the main stack.

## Using DICOM with MINC or NIfTI

For DICOM images of the head, chest, or abdomen the *x* coordinate
increases from right to left, the *y* coordinate increases from anterior
to posterior, and the *z* coordinate increases from inferior to
superior.  This is often referred to as the LPS coordinate system, and
refers to the coordinates that are achieved after the image is been
transformed via the PatientMatrix.  The NIfTI and MINC file formats use
a coordinate system where *x* increases to the right and *y* increases
to the front (anterior).  Hence, the coordinate system is rotated by 180
degrees as compared to DICOM.

The vtkDICOMToRAS filter can adjust a DICOM image so that it shares the
same coordinate system as MINC and NIfTI.  It can also be used to do
the reverse, and convert a NIfTI or MINC image to DICOM coordinates.

~~~~~~~~{.cpp}
  vtkNew<vtkDICOMToRAS> converter;
  converter->SetInputConnection(reader->GetOutputPort());
  converter->SetPatientMatrix(reader->GetPatientMatrix());
  converter->SetAllowRowReordering(true);
  converter->SetAllowColumnReordering(false);

  converter->UpdateMatrix();
  vtkMatrix4x4 *matrix = converter->GetRASMatrix();
  converter->Update();
  vtkImageData *image = converter->GetOutput();
~~~~~~~~