Skip to content

The DICOM row/column trap

You wrote arr[x, y] to index column x, row y of a CT — and the anatomy came out transposed. Or you read PixelSpacing, assumed the first number was the x spacing, and your resampling stretched the image the wrong way. Both are the same bug wearing two disguises, and both are failure modes of the confusion this chapter opened with: indices are positions in an array, and nothing forces the names of those index axes to line up across tools. DICOM’s row/column naming crosses itself, and it crosses again against NumPy and SimpleITK. This section is the one place to nail the conventions down so the transpose happens on paper instead of in a pipeline.

The same voxel, three naming systems

One physical voxel is described by three different tools using three different index orders. They all mean the same voxel, but they index it differently:

DICOM pixel (i, j) = (column i, row j)
SimpleITK index (x, y, z) = (column i, row j, slice k) # NOT NumPy order
NumPy array [k, j, i] = (slice k, row j, column i)
# ^ for the array returned by sitk.GetArrayFromImage on a scalar 3D image

NumPy and SimpleITK index the same voxel in opposite orders. That is the bite — with one scoping rule: NumPy itself assigns no slice/row/column meaning to array axes. arr[k, j, i] is the order of the scalar 3D array produced by sitk.GetArrayFromImage(img), a conversion convention — not a property of every ndarray. (Likewise, DICOM’s i/j are the zero-based column/row indices of the transmitted image matrix; “row” and “column” there name matrix positions, not anatomical directions.)

One highlighted DICOM pixel is represented as (i,j), NumPy arr[k,j,i], and SimpleITK GetPixel(x=i,y=j,z=k), with PixelSpacing cross-coupling.

The repeated V is one physical voxel; only its notation changes across DICOM, NumPy and SimpleITK.

The cross-coupling, spelled out

The confusion has two layers. Layer 1 — the names cross. In the geometry of the previous section:

  • ImageOrientationPatient first triplet = row direction = direction of increasing column index i.
  • PixelSpacing first value = row spacing = step when row index j increases by 1.

So the row direction cosine pairs with the column spacing (it is the step along i), and the column direction cosine pairs with the row spacing (the step along j). The word “row” appears in both but refers to different axes. This is not pedantry — getting it backwards resamples or registers along the wrong axis. A PixelSpacing of [0.8, 0.7] makes the point: the step when the column index i increases by 1 is 0.7 — the second value, the column spacing — because i increases along a row. Taking the first value instead stretches or compresses the resample along the wrong axis, a silent error that looks fine until you overlay a mask.

Layer 2 — the index orders cross between libraries. A DICOM pixel at (i = column, j = row) lives, after sitk.GetArrayFromImage(img) converts a scalar 3D image, in a NumPy array (slices-first, image order) as numpy[k, j, i], i.e. numpy[slice, row, column]. So arr[0] is slice 0, and within a slice arr[k, j, i] indexes row j, column i — the transpose of the naive arr[x, y].

DICOM: pixel (i=col, j=row)
| same voxel
+--> NumPy: arr[k, j, i] = slice, row, column
|
+--> SimpleITK: GetPixel(x=i, y=j, z=k)
= column, row, slice
NumPy and SimpleITK: opposite index order

When in doubt, hold this in your head:

DICOM i is column, j is row; PixelSpacing is [row, col]; NumPy is [k, j, i] — for the scalar 3D array GetArrayFromImage returns; SimpleITK is (x=i, y=j, z=k).

And the operational version: always print GetSpacing()/GetOrigin()/GetDirection() rather than assuming, and convert explicitly with sitk.GetArrayFromImage / GetImageFromArray at the NumPy↔SimpleITK boundary. GetImageFromArray creates a new image with default origin, spacing, and direction: copy or explicitly set the intended origin, spacing, and direction before treating it as spatially corresponding to the array’s source.

A concrete check you can run

import SimpleITK as sitk
img = sitk.ReadImage("series_dir") # or build from DICOM
arr = sitk.GetArrayFromImage(img) # -> NumPy [k, j, i]
print(img.GetSize()) # (x=cols, y=rows, z=slices) -- SimpleITK order
print(arr.shape) # (slices, rows, cols) -- NumPy order (reversed)
print(img.GetSpacing()) # (sx, sy, sz) in mm, LPS

Note the reversal: img.GetSize() is (x, y, z) while arr.shape is (z, y, x). The voxel at SimpleITK (x=10, y=20, z=5) is arr[5, 20, 10]. If you forget the reversal you have silently transposed the volume. This reversal is the GetArrayFromImage conversion convention for a scalar 3D image — not an intrinsic NumPy memory-order or indexing rule for arbitrary arrays.

Recognising orientations from the cosines

The two IOP triplets tell you which way the row/column axes point in the patient. For the standard axial orientation, increasing the column index i points toward patient left (+x in LPS — right-to-left across the patient), and increasing the row index j points toward posterior (+y), giving the axial IOP used in the worked example of the previous section:

ImageOrientationPatient = [1, 0, 0, 0, 1, 0] # row=+x(L), col=+y(P), axial plane

A coronal or sagittal series has different cosines; an oblique series (acquired at a tilt) has non-axis cosines. Never assume “axial” — read the cosines, because the direction matrix is part of the geometry that decides whether two objects share space (the shared-space section).

The conventions in this section keep the in-plane geometry honest. A 3D volume has one more geometric question to settle before it can be trusted: in which order do the slices stack? The answer is geometry again, and the next section derives it.

Sources

  • DICOM PS3.3, Image Plane Module (C.7.6.2) and Image Position and Image Orientation (C.7.6.2.1.1, Equation C.7.6.2.1-1) — pixel index (column i, row j) semantics, ImageOrientationPatient, and PixelSpacing ordering. (current edition)
  • SimpleITK, fundamental concepts — index/physical-space duality, GetPixel, GetSize, GetSpacing, and the NumPy↔SimpleITK array/image conversion boundary with its opposite dimension order. (docs)

© 2026 Adrian Tkacz · BioMedical-IT
Original text and figures are licensed under the Creative Commons Attribution-NonCommercial 4.0 International License (CC BY-NC 4.0), unless otherwise noted. Please credit Adrian Tkacz / BioMedical-IT and link to the original source. Third-party materials retain their respective rights and licenses.