Order slices without trusting filenames
You have 320 DICOM files for one series. Their names carry no spatial meaning,
and InstanceNumber has gaps. Which order do you stack them in to get a correct
3D volume? Nothing in the previous sections answers this yet: the affine placed
each slice in the patient, but a volume is a stack, and stacking in the wrong
order breaks the correspondence between array index and patient-space
progression — the z-dependent calculations that follow can then be silently
wrong. The answer this section develops is to compute the order from physical
position — geometry, not labels.
Why InstanceNumber and filenames lie
Normatively, InstanceNumber (0020,0013) is modest: DICOM defines it only as
“a number that identifies this image” within its series. It is not a spatial
coordinate, and nothing in the standard makes it encode the patient-space
ordering a volume needs — contiguity, anatomical monotonicity, or stability
across re-exports are outside its contract, and its values are meaningful only
within one series. Filenames are even less spatial: a re-export or a download
can rename or reorder files, and an ordinary lexical sort puts slice_10 before
slice_2. Neither label carries image-plane geometry, so neither can establish
anatomical order.
If you stack by InstanceNumber or filename and get lucky, the volume looks fine.
If you get unlucky, slices are subtly misordered: the array index no longer
tracks patient-space progression, and geometry-dependent calculations —
z-neighbourhoods, surfaces, textures — can be silently corrupted. Not every
quantity need change — a voxel count multiplied by a fixed voxel volume, for
example, is permutation-invariant — and nothing in the resulting array announces
the damage either way. It is the mask-in-the-wrong-space failure of the chapter
opening, one dimension over. The fix is to stop trusting labels and derive the
order from where each slice physically sits.
The slice normal
The third axis of a CT volume is the slice normal n — the direction in which
slices stack. It is the cross product of the row and column direction cosines:
n = cross([rx, ry, rz], [cx, cy, cz])For an axial series in the standard orientation r = [1, 0, 0], c = [0, 1, 0],
n evaluates to [0, 0, 1] — roughly Superior, the direction “up the stack”.
The sign is fixed by the ordered direction cosines themselves, not by acquisition
chronology, and an arbitrary axial orientation is not guaranteed to point the
normal toward Superior — which is why the method projects onto the computed n
rather than assuming +z. Everything in this section reduces to projecting slice
positions onto n.
Order by projection onto the slice normal
Each slice carries its own ImagePositionPatient p_k — the physical position of
its top-left voxel, as established in the affine. Project that position onto the
slice normal to get a single scalar “height” along the stack direction:
t_k = dot(p_k, n)Then sort slices by t_k ascending. For parallel slices sharing a valid,
consistent orientation, the result is a stack ordered consistently along one
physical direction, independent of filenames and of InstanceNumber.
slice A IPP = [..., 40.0] t = 40.0 | vslice B IPP = [..., 41.25] t = 41.25 | vslice C IPP = [..., 42.5] t = 42.5
sort by t ascending = order along the stack axisProjection and sorting are necessary, but they are not sufficient on their own. A sound stack also needs its geometry checked: direction cosines finite, orthonormal, and consistent across slices; projected positions unique — duplicate or near-duplicate positions are ambiguous and cannot be ordered against each other; in-plane dimensions and pixel spacing consistent; localizer (scout) slices excluded. And sorting makes a re-export harmless only if that re-export preserved the geometry: it orders whatever positions are encoded, right or wrong.
This is exactly what TRACE-CT’s
compute_slice_normal() and project_position() compute, and what
load_ct_volume_explicit() uses to order the slice metadata; the loader then
validates dimensions, orientation, pixel spacing, unique positions, and regular
spacing before it allocates and fills the pixel volume.
Worked example: three slices
As a synthetic derivation — a constructed example, not a measured series —
suppose three slices (axial, so n ≈ [0,0,1]) have these positions:
| Slice file | ImagePositionPatient (z) |
t = dot(p, n) |
|---|---|---|
img_002.dcm |
42.5 | 42.5 |
img_001.dcm |
40.0 | 40.0 |
img_003.dcm |
41.25 | 41.25 |
Filenames say 1, 2, 3 but positions are scrambled. Sorting by t gives the
physical stack order: img_001 (40.0) → img_003 (41.25) → img_002 (42.5). The
spacing between consecutive slices is the difference of consecutive t values:
1.25 mm and 1.25 mm — a regular stack.
A cautionary, hypothetical variant shows why the geometric method earns its keep.
Suppose a colleague sorts by InstanceNumber on a cleanly exported series and
the volume comes out correct — identical, in fact, to the geometric ordering.
That agreement demonstrates only that the label and the geometry agree in that
one series; it is not evidence that the label is generally safe. Geometric
ordering costs a few lines of code and removes the question of whether the label
happens to agree this time.
Irregular spacing: when the stack is not a volume
Once you have the sorted t_k, compute the gaps t_{k+1} − t_k. If they are all
approximately equal — within a tolerance you prespecify for the encoding
precision and the downstream use — the stack can represent one regular third
axis. If they vary, you have irregular spacing: the regular-grid assumptions
behind voxel-wise 3D operations no longer hold, distances along z are not what
the array indices imply, and any measurement or resampling that treats the stack
as uniform will be quietly wrong. Unequal gaps can arise from missing slices, an
interleaved acquisition, or other acquisition and selection effects; the pattern
of gaps alone does not diagnose which, and in particular does not by itself mean
variable slice thickness. Equal gaps are also necessary but not sufficient: a
regular grid additionally requires consistent orientation, consistent in-plane
spacing and dimensions, unique positions, and finite geometry. Options then:
- resample onto a regular grid (if the physics justifies it — the resampling section);
- or treat the slices as a non-uniform stack and reject voxel-wise 3D operations that assume regularity.
Either option is legitimate only after the stack’s provenance and physical appropriateness have been assessed.
Either way, measure the gaps; do not assume them — and cross-check. Compute the
adjacent projected centre gaps and compare them with SpacingBetweenSlices
(0018,0088) where the series declares it: that attribute has a normative
centre-to-centre meaning for adjacent slices, while the projected differences are
the spacing implied by the encoded positions under a consistent orientation.
Neither field is infallible ground truth; they are two encodings that can
conflict, and material disagreement is a reason to investigate or reject the
series, not to pick a favourite. This is the discipline TRACE-CT’s
load_ct_volume_explicit() implements: it rejects duplicate positions and
irregular projected spacing before returning a usable volume.
With slice order and grid regularity settled, the volume is geometrically sound as a
standalone object. The next question is relational: when does a segmentation truly
share the CT’s space — and what does
FrameOfReferenceUID actually prove about that?
Sources
- DICOM PS3.3, General Image Module (C.7.6.1, Table C.7-9) —
InstanceNumber(0020,0013) defined as a number that identifies this image. (current edition) - DICOM PS3.3, Image Plane Module (C.7.6.2) —
ImagePositionPatient,ImageOrientationPatient, andSpacingBetweenSlices(0018,0088) centre-to-centre adjacent-slice spacing. (current edition) - SimpleITK, fundamental concepts — origin, spacing, direction, and size as the metadata of a regular image geometry. (docs)
- TRACE-CT — slice-ordering and CT-volume validation implementation (
compute_slice_normal(),project_position(),load_ct_volume_explicit()), bounded to the linked commit. (commit 7aaf308)
© 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.