Skip to content

Commit 8471a37

Browse files
committed
DOC - mainly - clean up exposition of affines
1 parent a86048d commit 8471a37

File tree

6 files changed

+173
-103
lines changed

6 files changed

+173
-103
lines changed

doc/source/dicom/derivations/spm_dicom_orient.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ def numbered_vector(nrows, symbol_prefix):
3030
row_col_swap[:,1] = eye(4)[:,0]
3131

3232
# various worming matrices
33-
orient_pat = numbered_matrix(3, 2, 'O')
34-
orient_cross = numbered_vector(3, 'c')
33+
orient_pat = numbered_matrix(3, 2, 'F')
34+
orient_cross = numbered_vector(3, 'n')
35+
missing_r_col = numbered_vector(3, 'k')
3536
pos_pat_0 = numbered_vector(3, 'T^1')
3637
pos_pat_N = numbered_vector(3, 'T^N')
37-
pixel_spacing = symbols(('\Delta_{cols}', '\Delta_{rows}'))
38+
pixel_spacing = symbols(('\Delta{r}', '\Delta{c}'))
3839
NZ = Symbol('N')
39-
slice_thickness = Symbol('\Delta_{slices}')
40+
slice_thickness = Symbol('\Delta{s}')
4041

4142
R3 = orient_pat * np.diag(pixel_spacing)
4243
R = zeros((4,2))
@@ -49,12 +50,12 @@ def numbered_vector(nrows, symbol_prefix):
4950

5051
to_inv = zeros((4,4))
5152
to_inv[:,0] = x1
52-
to_inv[:,1] = symbols('ABCD')
53+
to_inv[:,1] = symbols('abcd')
5354
to_inv[0,2] = 1
5455
to_inv[1,3] = 1
5556
inv_lhs = zeros((4,4))
5657
inv_lhs[:,0] = y1
57-
inv_lhs[:,1] = symbols('EFGH')
58+
inv_lhs[:,1] = symbols('efgh')
5859
inv_lhs[:,2:] = R
5960

6061
def spm_full_matrix(x2, y2):
@@ -96,7 +97,6 @@ def spm_full_matrix(x2, y2):
9697
# ``pat_pos_N = aff * [[0,0,ZN-1,1]].T
9798
multi_aff = eye(4)
9899
multi_aff[:3,:2] = R3
99-
missing_r_col = numbered_vector(3, 's')
100100
trans_z_N = Matrix((0,0, NZ-1, 1))
101101
multi_aff[:3, 2] = missing_r_col
102102
multi_aff[:3, 3] = pos_pat_0
@@ -144,14 +144,14 @@ def my_latex(expr):
144144
print
145145
print ' 0B = ' + my_latex(one_based)
146146
print
147+
print ' ' + my_latex(solved)
148+
print
147149
print ' A_{multi} = ' + my_latex(multi_aff_solved)
148150
print ' '
149151
print ' A_{single} = ' + my_latex(single_aff)
150152
print
151153
print r' \left(\begin{smallmatrix}T^N\\1\end{smallmatrix}\right) = A ' + my_latex(trans_z_N)
152154
print
153-
print ' ' + my_latex(solved)
154-
print
155155
print ' A_j = A_{single} ' + my_latex(nz_trans)
156156
print
157157
print ' T^j = ' + my_latex(IPP_j)

doc/source/dicom/dicom_mosaic.rst

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -37,16 +37,16 @@ millimeters of the first voxel in the voxel volume (the voxel given by
3737
``voxel_array[0,0,0]``).
3838

3939
In the case of the mosaic, we have the first two columns of $R$ from the
40-
``ImageOrientationPatient`` DICOM field. To make a full rotation
41-
matrix, we can generate the last column from the cross product of the
42-
first two. However, Siemens defines, in its private :ref:`csa-header`,
43-
a ``SliceNormalVector`` which gives the third column, but possibly with
44-
a z flip, so that $R$ is orthogonal, but not a rotation matrix (it has a
45-
determinant of < 0).
46-
47-
The first two values of $\mathbf{s}$ ($s_1, s_2$) are given by the second
48-
and first values in the ``PixelSpacing`` field, respectively. Why this
49-
order? - see :ref:`ij-transpose`. We get $s_3$ (the slice scaling
40+
$F$ - the left/right flipped version of the ``ImageOrientationPatient``
41+
DICOM field described in :ref:`dicom-affines-reloaded`. To make a full
42+
rotation matrix, we can generate the last column from the cross product
43+
of the first two. However, Siemens defines, in its private
44+
:ref:`csa-header`, a ``SliceNormalVector`` which gives the third column,
45+
but possibly with a z flip, so that $R$ is orthogonal, but not a
46+
rotation matrix (it has a determinant of < 0).
47+
48+
The first two values of $\mathbf{s}$ ($s_1, s_2$) are given by the
49+
``PixelSpacing`` field. We get $s_3$ (the slice scaling
5050
value) from ``SpacingBetweenSlices``.
5151

5252
The :ref:`spm-dicom` code has a comment saying that mosaic DICOM imagqes
@@ -59,44 +59,38 @@ the top left voxel, where the slice size used for this adjustment is the
5959
size of the mosaic, before it has been unpacked. Let's call the correct
6060
position in millimeters of the center of the first slice $\mathbf{c} =
6161
[c_x, c_y, c_z]$. We have the derived $RS$ matrix from the calculations
62-
in :ref:`dicom-orientation`. The unpacked (eventual, real) slice
63-
dimensions are $(rd_{rows}, rd_{cols})$ and the mosaic dimensions are
64-
$(md_{rows}, md_{cols})$. The ``ImagePositionPatient`` vector
65-
$\mathbf{i}$ resulted from:
62+
above. The unpacked (eventual, real) slice dimensions are $(rd_{rows},
63+
rd_{cols})$ and the mosaic dimensions are $(md_{rows}, md_{cols})$. The
64+
``ImagePositionPatient`` vector $\mathbf{i}$ resulted from:
6665

6766
.. math::
6867
6968
\mathbf{i} = \mathbf{c} + RS
70-
\begin{bmatrix} -(md_{cols}-1) / 2\\
71-
-(md_{rows}-1) / 2\\
69+
\begin{bmatrix} -(md_{rows}-1) / 2\\
70+
-(md_{cols}-1) / 2\\
7271
0 \end{bmatrix}
7372
74-
Note that the vector has $md_{cols}$ is the first value, $md_{rows}$ is
75-
the second. This follows from the :ref:`ij-transpose`. The transpose
76-
means that the first input coordinate is the column index, and the
77-
second is the row index.
78-
7973
To correct the faulty translation, we reverse it, and add the correct
8074
translation for the unpacked slice size $(rd_{rows}, rd_{cols})$, giving
8175
the true image position $\mathbf{t}$:
8276

8377
.. math::
8478
8579
\mathbf{t} = \mathbf{i} -
86-
(RS \begin{bmatrix} -(md_{cols}-1) / 2\\
87-
-(md_{rows}-1) / 2\\
80+
(RS \begin{bmatrix} -(md_{rows}-1) / 2\\
81+
-(md_{cols}-1) / 2\\
8882
0 \end{bmatrix}) +
89-
(RS \begin{bmatrix} -(rd_{cols}-1) / 2\\
90-
-(rd_{rows}-1) / 2\\
83+
(RS \begin{bmatrix} -(rd_{rows}-1) / 2\\
84+
-(rd_{cols}-1) / 2\\
9185
0 \end{bmatrix})
9286
9387
Because of the final zero in the voxel translations, this simplifies to:
9488

9589
.. math::
9690
9791
\mathbf{t} = \mathbf{i} +
98-
Q \begin{bmatrix} (md_{cols} - rd_{cols}) / 2 \\
99-
(md_{rows} - rd_{rows}) / 2 \end{bmatrix}
92+
Q \begin{bmatrix} (md_{rows} - rd_{rowss}) / 2 \\
93+
(md_{cols} - rd_{cols}) / 2 \end{bmatrix}
10094
10195
where:
10296

doc/source/dicom/dicom_orientation.rst

Lines changed: 116 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -171,14 +171,84 @@ given $M$ above:
171171
172172
M_{pixar} = M \left(\begin{smallmatrix}0 & 1 & 0 & 0\\1 & 0 & 0 & 0\\0 & 0 & 1 & 0\\0 & 0 & 0 & 1\end{smallmatrix}\right)
173173
174-
The affines below do not take this premultiplication into account - that
175-
is, they assume that you have either already applied it, or that you
176-
have transposed ``pixel_array``.
174+
.. _dicom-affines-reloaded:
177175

178-
.. _dicoms-and-affines:
176+
DICOM affines again
177+
===================
179178

180-
Getting the affine transformation from DICOM files and file lists
181-
=================================================================
179+
The :ref:`ij-transpose` is rather confusing, so we're going to rephrase
180+
the affine mapping; we'll use $r$ for the row index (instead of $j$
181+
above), and $c$ for the column index (instead of $i$).
182+
183+
Next we define a flipped version of 'ImageOrientationPatient', $F$, that
184+
has flipped columns. Thus if the vector of 6 values in
185+
'ImageOrientationPatient' are $(i_1 .. i_6)$, then:
186+
187+
.. math::
188+
189+
F = \begin{bmatrix} i_4 & i_1 \\
190+
i_5 & i_2 \\
191+
i_6 & i_3 \end{bmatrix}
192+
193+
Now the first column of F contains what the DICOM docs call the 'column
194+
(Y) direction cosine', and second column contains the 'row (X) direction
195+
cosine'. We prefer to think of these as (respectively) the row index
196+
direction cosine and the column index direction cosine.
197+
198+
Now we can rephrase the DICOM affine mapping with:
199+
200+
.. _dicom-slice-affine:
201+
202+
DICOM affine formula
203+
====================
204+
205+
.. math::
206+
207+
\begin{bmatrix} P_x\\
208+
P_y\\
209+
P_z\\
210+
1 \end{bmatrix} =
211+
\begin{bmatrix} F_{11}\Delta{r} & F_{12}\Delta{c} & 0 & S_x \\
212+
F_{21}\Delta{r} & F_{22}\Delta{c} & 0 & S_y \\
213+
F_{31}\Delta{r} & F_{32}\Delta{c} & 0 & S_z \\
214+
0 & 0 & 0 & 1 \end{bmatrix}
215+
\begin{bmatrix} r\\
216+
c\\
217+
0\\
218+
1 \end{bmatrix}
219+
= A
220+
\begin{bmatrix} r\\
221+
c\\
222+
0\\
223+
1 \end{bmatrix}
224+
225+
Where:
226+
227+
* $P_{xyz}$ : The coordinates of the voxel (c, r) in the frame's image
228+
plane in units of mm.
229+
* $S_{xyz}$ : The three values of the Image Position (Patient)
230+
(0020,0032) attributes. It is the location in mm from the origin of
231+
the RCS.
232+
* $F_{:,1}$ : The values from the column (Y) direction cosine of the
233+
Image Orientation (Patient) (0020,0037) attribute - see above.
234+
* $F_{:,2}$ : The values from the row (X) direction cosine of the Image
235+
Orientation (Patient) (0020,0037) attribute - see above.
236+
* $r$ : Row index to the image plane. The first row index is zero.
237+
* $\Delta{r}$ - Row pixel resolution of the Pixel Spacing (0028,0030)
238+
attribute in units of mm.
239+
* $c$ : Column index to the image plane. The first column is index zero.
240+
* $\Delta{c}$: Column pixel resolution of the Pixel Spacing (0028,0030)
241+
attribute in units of mm.
242+
243+
For later convenience we also define values useful for 3D volumes:
244+
245+
* $s$ : slice index to the slice plane. The first slice index is zero.
246+
* $\Delta{s}$ - spacing in mm between slices.
247+
248+
.. _dicom-3d-affines:
249+
250+
Getting a 3D affine from a DICOM slice or list of slices
251+
========================================================
182252

183253
Let us say, we have a single DICOM file, or a list of DICOM files that
184254
we believe to be a set of slices from the same volume. We'll call the
@@ -190,50 +260,48 @@ In the *multi slice* case, we can assume that the
190260
We want to get the affine transformation matrix $A$ that maps from voxel
191261
coordinates in the DICOM file(s), to mm in the :ref:`dicom-pcs`.
192262

193-
By voxel coordinates, we mean *transposed coordinates* - see
194-
:ref:`ij-transpose` above.
263+
By voxel coordinates, we mean coordinates of form $(r, c, s)$ - the row,
264+
column and slice indices - as for the :ref:`dicom-slice-affine`.
195265

196266
In the single slice case, the voxel coordinates are just the indices
197267
into the pixel array, with the third (slice) coordinate always being 0.
268+
198269
In the multi-slice case, we have arranged the slices in ascending or
199270
descending order, where slice numbers range from 0 to $N-1$ - where $N$
200271
is the number of slices - and the slice coordinate is a number on this
201272
scale.
202273

203-
We know, from the formula above, that the first, second and fourth
204-
columns in $A$ are given directly by the formula in
205-
:ref:`dicom-orientation` - from the 'ImageOrientationPatient',
206-
(reversed) 'PixelSpacing' and 'ImagePositionPatient' field of the first
207-
(or only) slice.
274+
We know, from :ref:`dicom-slice-affine`, that the first, second and
275+
fourth columns in $A$ are given directly by the (flipped)
276+
'ImageOrientationPatient', 'PixelSpacing' and 'ImagePositionPatient'
277+
field of the first (or only) slice.
208278

209279
Our job then is to fill the first three rows of the third column of $A$.
210-
Let's call this the vector $\mathbf{s}$ with values $s_1, s_2, s_3$.
280+
Let's call this the vector $\mathbf{k}$ with values $k_1, k_2, k_3$.
211281

212282
.. _dicom-affine-defs:
213283

214284
DICOM affine Definitions
215285
------------------------
216286

217-
Let $O$ be the DICOM orientation patient field, reorganized to the (3,2)
218-
matrix it represents (see :ref:`dicom-orientation`). Let $T^1$ be the 3
219-
element vector of the 'ImagePositionPatient' field of the first header
220-
in the list of headers for this volume. Let $T^N$ be the
221-
'ImagePositionPatient' vector for the last header in the list for this
222-
volume, if there is more than one header in the volume. Let
223-
$\Delta_{cols}$ and $\Delta_{rows}$ be the two values in the
224-
'PixelSpacing' field. Let $\Delta_{slices}$ be the value for the
225-
'SliceThickness' field, if present, otherwise $\Delta_{slices} = 1$. Let
226-
vector $\mathbf{c} = (c_1, c_2, c_3)$ be the result of taking the cross
227-
product of the two columns of $O$.
287+
See also the definitions in :ref:`dicom-slice-affine`. In addition
288+
289+
* $T^1$ is the 3 element vector of the 'ImagePositionPatient' field of
290+
the first header in the list of headers for this volume.
291+
* $T^N$ is the 'ImagePositionPatient' vector for the last header in the
292+
list for this volume, if there is more than one header in the volume.
293+
* vector $\mathbf{n} = (n_1, n_2, n_3)$ is the result of taking the
294+
cross product of the two columns of $F$ from
295+
:ref:`dicom-slice-affine`.
228296

229297
Derivations
230298
-----------
231299

232-
For the single slice case we just fill $\mathbf{s}$ with $\mathbf{c} \cdot
233-
\Delta_{slices}$ - on the basis that the Z dimension should be
300+
For the single slice case we just fill $\mathbf{k}$ with $\mathbf{n} \cdot
301+
\Delta{s}$ - on the basis that the Z dimension should be
234302
right-handed orthogonal to the X and Y directions.
235303

236-
For the multi-slice case, we can fill in $\mathbf{s}$ by using the information
304+
For the multi-slice case, we can fill in $\mathbf{k}$ by using the information
237305
from $T^N$, because $T^N$ is the translation needed to take the
238306
first voxel in the last (slice index = $N-1$) slice to mm space. So:
239307

@@ -245,20 +313,20 @@ From this it follows that:
245313

246314
.. math::
247315
248-
\begin{Bmatrix}s_{{1}} : \frac{T^{1}_{{1}} - T^{N}_{{1}}}{1 - N}, & s_{{2}} : \frac{T^{1}_{{2}} - T^{N}_{{2}}}{1 - N}, & s_{{3}} : \frac{T^{1}_{{3}} - T^{N}_{{3}}}{1 - N}\end{Bmatrix}
316+
\begin{Bmatrix}k_{{1}} : \frac{T^{1}_{{1}} - T^{N}_{{1}}}{1 - N}, & k_{{2}} : \frac{T^{1}_{{2}} - T^{N}_{{2}}}{1 - N}, & k_{{3}} : \frac{T^{1}_{{3}} - T^{N}_{{3}}}{1 - N}\end{Bmatrix}
249317
250318
and therefore:
251319

252-
.. _dicom-affine-formulae:
320+
.. _dicom-3d-affine-formulae:
253321

254-
DICOM affine formulae
255-
---------------------
322+
3D ffine formulae
323+
-----------------
256324

257325
.. math::
258326
259-
A_{multi} = \left(\begin{smallmatrix}O_{{11}} \Delta_{cols} & O_{{12}} \Delta_{rows} & \frac{T^{1}_{{1}} - T^{N}_{{1}}}{1 - N} & T^{1}_{{1}}\\O_{{21}} \Delta_{cols} & O_{{22}} \Delta_{rows} & \frac{T^{1}_{{2}} - T^{N}_{{2}}}{1 - N} & T^{1}_{{2}}\\O_{{31}} \Delta_{cols} & O_{{32}} \Delta_{rows} & \frac{T^{1}_{{3}} - T^{N}_{{3}}}{1 - N} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{smallmatrix}\right)
327+
A_{multi} = \left(\begin{smallmatrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \frac{T^{1}_{{1}} - T^{N}_{{1}}}{1 - N} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \frac{T^{1}_{{2}} - T^{N}_{{2}}}{1 - N} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \frac{T^{1}_{{3}} - T^{N}_{{3}}}{1 - N} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{smallmatrix}\right)
260328
261-
A_{single} = \left(\begin{smallmatrix}O_{{11}} \Delta_{cols} & O_{{12}} \Delta_{rows} & c_{{1}} \Delta_{slices} & T^{1}_{{1}}\\O_{{21}} \Delta_{cols} & O_{{22}} \Delta_{rows} & c_{{2}} \Delta_{slices} & T^{1}_{{2}}\\O_{{31}} \Delta_{cols} & O_{{32}} \Delta_{rows} & c_{{3}} \Delta_{slices} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{smallmatrix}\right)
329+
A_{single} = \left(\begin{smallmatrix}F_{{11}} \Delta{r} & F_{{12}} \Delta{c} & \Delta{s} n_{{1}} & T^{1}_{{1}}\\F_{{21}} \Delta{r} & F_{{22}} \Delta{c} & \Delta{s} n_{{2}} & T^{1}_{{2}}\\F_{{31}} \Delta{r} & F_{{32}} \Delta{c} & \Delta{s} n_{{3}} & T^{1}_{{3}}\\0 & 0 & 0 & 1\end{smallmatrix}\right)
262330
263331
See :download:`derivations/spm_dicom_orient.py` for the derivations and
264332
some explanations.
@@ -305,7 +373,7 @@ and 'ImagePositionPatient' for $j$ is:
305373

306374
.. math::
307375
308-
T^j = \left(\begin{smallmatrix}T^{1}_{{1}} + c_{{1}} d \Delta_{slices}\\T^{1}_{{2}} + c_{{2}} d \Delta_{slices}\\T^{1}_{{3}} + c_{{3}} d \Delta_{slices}\end{smallmatrix}\right)
376+
T^j = \left(\begin{smallmatrix}T^{1}_{{1}} + \Delta{s} d n_{{1}}\\T^{1}_{{2}} + \Delta{s} d n_{{2}}\\T^{1}_{{3}} + \Delta{s} d n_{{3}}\end{smallmatrix}\right)
309377
310378
Remember that the third column of $A$ gives the vector resulting from a
311379
unit change in the slice voxel coordinate. So, the
@@ -315,18 +383,24 @@ $\mathbf{a}$ is the position of the first voxel in some slice (here
315383
slice 1, therefore $\mathbf{a} = T^1$) and $\mathbf{b}$ is $d$ times the
316384
third colum of $A$. Obviously $d$ can be negative or positive. This
317385
leads to various ways of recovering something that is proportional to
318-
$d$ plus a constant. SPM takes the inner product of $T^j$ with the unit
319-
vector component of third column of $A_j$ - in the descriptions here,
320-
this is the vector $\mathbf{c}$:
386+
$d$ plus a constant. The algorithm suggested in this `ITK post on
387+
ordering slices`_ - and the one used by SPM - is to take the inner
388+
product of $T^j$ with the unit vector component of third column of
389+
$A_j$ - in the descriptions here, this is the vector $\mathbf{n}$:
390+
391+
.. _ITK post on ordering slices: http://www.itk.org/pipermail/insight-users/2003-September/004762.html
321392

322393
.. math::
323394
324-
T^j \cdot \mathbf{c} = \left(\begin{smallmatrix}c_{{1}} T^{1}_{{1}} + c_{{2}} T^{1}_{{2}} + c_{{3}} T^{1}_{{3}} + d \Delta_{slices} c_{{1}}^{2} + d \Delta_{slices} c_{{2}}^{2} + d \Delta_{slices} c_{{3}}^{2}\end{smallmatrix}\right)
395+
T^j \cdot \mathbf{c} = \left(\begin{smallmatrix}T^{1}_{{1}} n_{{1}} + T^{1}_{{2}} n_{{2}} + T^{1}_{{3}} n_{{3}} + \Delta{s} d n_{{1}}^{2} + \Delta{s} d n_{{2}}^{2} + \Delta{s} d n_{{3}}^{2}\end{smallmatrix}\right)
396+
397+
This is the distance of 'ImagePositionPatient' along the slice direction
398+
cosine.
325399

326400
The unknown $T^1$ terms pool into a constant, and the operation has the
327-
neat feature that, because the $c_N^2$ terms, by definition, sum to 1,
328-
the whole can be expressed as $\lambda + \Delta_{slices} d$ - i.e. it is
329-
equal to the slice voxel size ($\Delta_{slices}$) multiplied by $d$,
401+
neat feature that, because the $n_{123}^2$ terms, by definition, sum to 1,
402+
the whole can be expressed as $\lambda + \Delta{s} d$ - i.e. it is
403+
equal to the slice voxel size ($\Delta{s}$) multiplied by $d$,
330404
plus a constant.
331405

332406
Again, see :download:`derivations/spm_dicom_orient.py` for the derivations.

0 commit comments

Comments
 (0)