.. _dicom-mosaic:
=======================
Siemens mosaic format
=======================
Siemens mosaic format is a way of storing a 3D image in a DICOM_ image
file. The simplest DICOM_ images only knows how to store 2D files. For
example, a 3D image in DICOM is usually stored as a series of 2D slices,
each slices as a separate DICOM image. . Mosaic format stores the 3D
image slices as a 2D grid - or mosaic.
For example here are the pixel data as loaded directly from a DICOM image
with something like::
import matplotlib.pylab as plt
import dicom
dcm_data = dicom.read_file('my_file.dcm')
plt.imshow(dcm_data.pixel_array)
.. image:: mosaic_grid.png
Getting the slices from the mosaic
==================================
The apparent image in the DICOM file is a 2D array that consists of blocks,
that are the output 2D slices. Let's call the original array the *slab*, and
the contained slices *slices*. The slices are of pixel dimension
``n_slice_rows`` x ``n_slice_cols``. The slab is of pixel dimension
``n_slab_rows`` x ``n_slab_cols``. Because the arrangement of blocks in the
slab is defined as being square, the number of blocks per slab row and slab
column is the same. Let ``n_blocks`` be the number of blocks contained in the
slab. There is also ``n_slices`` - the number of slices actually collected,
some number <= ``n_blocks``. We have the value ``n_slices`` from the
'NumberOfImagesInMosaic' field of the Siemens private (CSA) header.
``n_row_blocks`` and ``n_col_blocks`` are therefore given by
``ceil(sqrt(n_slices))``, and ``n_blocks`` is ``n_row_blocks ** 2``. Also
``n_slice_rows == n_slab_rows / n_row_blocks``, etc. Using these numbers we
can therefore reconstruct the slices from the 2D DICOM pixel array.
DICOM orientation for mosaic
============================
See :ref:`dicom-pcs` and :ref:`dicom-orientation`. We want a 4 x 4
affine $A$ that will take us from (transposed) voxel coordinates in the
DICOM image to mm in the :ref:`dicom-pcs`. See :ref:`ij-transpose` for
what we mean by transposed voxel coordinates.
We can think of the affine $A$ as the (3,3) component, $RS$, and a (3,1)
translation vector $\mathbf{t}$. $RS$ can in turn be thought of as the
dot product of a (3,3) rotation matrix $R$ and a scaling matrix $S$,
where ``S = diag(s)`` and $\mathbf{s}$ is a (3,) vector of voxel sizes.
$\mathbf{t}$ is a (3,1) translation vector, defining the coordinate in
millimeters of the first voxel in the voxel volume (the voxel given by
``voxel_array[0,0,0]``).
In the case of the mosaic, we have the first two columns of $R$ from the
$F$ - the left/right flipped version of the ``ImageOrientationPatient``
DICOM field described in :ref:`dicom-affines-reloaded`. To make a full
rotation matrix, we can generate the last column from the cross product
of the first two. However, Siemens defines, in its private
:ref:`csa-header`, a ``SliceNormalVector`` which gives the third column,
but possibly with a z flip, so that $R$ is orthogonal, but not a
rotation matrix (it has a determinant of < 0).
The first two values of $\mathbf{s}$ ($s_1, s_2$) are given by the
``PixelSpacing`` field. We get $s_3$ (the slice scaling
value) from ``SpacingBetweenSlices``.
The :ref:`spm-dicom` code has a comment saying that mosaic DICOM images
have an incorrect ``ImagePositionPatient`` field. The
``ImagePositionPatient`` field usually gives the $\mathbf{t}$ vector.
The comments imply that Siemens has derived ``ImagePositionPatient``
from the (correct) position of the center of the first slice (once the
mosaic has been unpacked), but has then adjusted the vector to point to
the top left voxel, where the slice size used for this adjustment is the
size of the mosaic, before it has been unpacked. Let's call the correct
position in millimeters of the center of the first slice $\mathbf{c} =
[c_x, c_y, c_z]$. We have the derived $RS$ matrix from the calculations
above. The unpacked (eventual, real) slice dimensions are $(rd_{rows},
rd_{cols})$ and the mosaic dimensions are $(md_{rows}, md_{cols})$. The
``ImagePositionPatient`` vector $\mathbf{i}$ resulted from:
.. math::
\mathbf{i} = \mathbf{c} + RS
\begin{bmatrix} -(md_{rows}-1) / 2\\
-(md_{cols}-1) / 2\\
0 \end{bmatrix}
To correct the faulty translation, we reverse it, and add the correct
translation for the unpacked slice size $(rd_{rows}, rd_{cols})$, giving
the true image position $\mathbf{t}$:
.. math::
\mathbf{t} = \mathbf{i} -
(RS \begin{bmatrix} -(md_{rows}-1) / 2\\
-(md_{cols}-1) / 2\\
0 \end{bmatrix}) +
(RS \begin{bmatrix} -(rd_{rows}-1) / 2\\
-(rd_{cols}-1) / 2\\
0 \end{bmatrix})
Because of the final zero in the voxel translations, this simplifies to:
.. math::
\mathbf{t} = \mathbf{i} +
Q \begin{bmatrix} (md_{rows} - rd_{rowss}) / 2 \\
(md_{cols} - rd_{cols}) / 2 \end{bmatrix}
where:
.. math::
Q = \begin{bmatrix} rs_{11} & rs_{12} \\
rs_{21} & rs_{22} \\
rs_{31} & rs_{32} \end{bmatrix}
Data scaling
============
SPM gets the DICOM scaling, offset for the image ('RescaleSlope',
'RescaleIntercept'). It writes these scalings into the nifti_ header.
Then it writes the raw image data (unscaled) to disk. Obviously these
will have the current scalings applied when the nifti image is read again.
A comment in the code here says that the data are not scaled by the
maximum amount. I assume by this they mean that the DICOM scaling may
not be the maximum scaling, whereas the standard SPM image write is,
hence the difference, because they are using the DICOM scaling rather
then their own. The comment continues by saying that the scaling as
applied (the DICOM - not maximum - scaling) can lead to rounding errors
but that it will get around some unspecified problems.
.. include:: ../links_names.txt