Why a CT pixel is not automatically HU
Chapter 3 treats DICOM as the contract through which stored values acquire physical meaning from metadata and imaging objects are identified and organized into studies, series, and instances. Object identity remains distinct from spatial identity: this chapter is the packaging and identity contract between Chapter 2’s discussion of modality, signal, and contrast and Chapter 4’s treatment of physical geometry. The chapter ends by reading one real acquisition as a complete contract. It begins with the smallest version of that problem: why a stored value such as 37 is not automatically a CT measurement.
A CT slice can contain the stored integer 37. If code treats that number as a
tissue measurement, it may report +37 HU. In the encoding used by the example
below, the same stored value decodes to −987 HU. The difference is not a display
detail: the pixel array is the payload, while metadata supplies the contract that
gives the payload a physical interpretation.
The encoded value: 37 is not 37
Hounsfield units are a defined scale, but a DICOM object need not store that
scale’s value directly in each pixel. For classic CT Image Storage, the
CT Image Module
specifies BitsAllocated = 16, while PixelRepresentation declares whether
the stored samples are unsigned or signed. One permitted example uses unsigned
samples with an offset: a stored value of 0 can then represent a value near
−1000 HU without requiring a sign bit. Recovering the CT quantity requires
applying the linear mapping described by that object’s metadata:
HU = stored_pixel × RescaleSlope + RescaleInterceptUsing the declared example settings RescaleSlope = 1 and
RescaleIntercept = −1024:
stored pixel = 37HU = 37 × 1 + (−1024) = −987The integer 37 is therefore not +37 HU; under this mapping it is an
air-adjacent value. If code applies a threshold written in HU directly to the
stored value, the boundary moves to a different physical value, so voxels near
that boundary can be assigned to the wrong class. This is the bounded failure
to keep in mind; its extent depends on the threshold, transform, and image.
The two tags, precisely
RescaleSlope(0028,1053)— the multiplicative factor in the linear rescale. Read it from each object; do not assume it is1.RescaleIntercept(0028,1052)— the additive offset in that rescale. Read it from each object rather than assuming a particular value.
For the linear CT rescaling considered here, these attributes describe the linear-rescale alternative in the Modality LUT Module; they are not a LUT themselves. In an enhanced multi-frame object, the equivalent transform can be carried in a Pixel Value Transformation functional group, as part of the Enhanced CT Image IOD. For the scoped classic, non-localizer, non-multi-energy CT example, the output of this transform is HU. The practical point is that the mapping belongs to the DICOM contract carried alongside the pixel bytes; the array alone does not tell you which numerical scale it uses.
Why the contract matters
The failure is dangerous because it is numerically quiet. The array remains a valid array and may look entirely plausible, even though code is interpreting one scale as another. When a downstream operation expects HU but receives stored values, the error propagates into operations whose thresholds or statistics are defined in that physical scale:
- window/level or threshold operations use a different numerical scale;
- intensity discretisation for texture features bins the wrong grey levels;
- an HU clip such as the example
[-1000, +400]selects the wrong stored values; - in the declared slope-1, intercept-
−1024case, a reported “mean tumour attenuation” differs by exactly1024before any other processing. That arithmetic does not generalise to a different slope or intercept.
The error is silent because the array looks fine: it is a valid image, just
shifted or rescaled. A project-specific illustration appears in the
TRACE-CT learning map, where
load_ct_volume_explicit() applies volume[k] = pixel × slope + intercept
while building the volume. Later stages then receive the decoded volume rather
than stored pixels. The implementation is an example of enforcing the contract
at ingestion, not a substitute for reading the contract in each DICOM object.
A decoding check
An executable, versioned pydicom 3.0.2 example is kept in the
3D Slicer companion,
alongside the official pydicom pixel-data guide
and apply_rescale reference.
Its stable logic is to use the library’s decoded pixel_array, then apply the
object’s linear rescale or Modality LUT, and inspect the resulting values. The
result is a sanity check, not proof that the complete image contract is correct.
If outside-body air is present, inspect a representative region or distribution
there and compare it with the air reference near −1000 HU, while checking for
padding, artifacts, and the encoded transform. A region known to be water can be
compared with the 0 HU reference. Neither check makes the global hu.min() a
required air statistic. The arithmetic 0 + intercept is useful only as an
encoding-plausibility check: it predicts what stored zero would map to, but does
not show that stored zero was sampled in air or validate the image minimum.
In the declared slope-1, intercept-−1024 example, stored zero maps to −1024
by arithmetic. That is not a universal air band or a rule about scanners or
reconstructions, and a global minimum near zero does not by itself prove that
rescaling was skipped; padding, artifacts, anatomy, or clipping may control the
minimum.
The check exposes the central distinction: the array holds encoded intensity values, while the physical interpretation lives in metadata. The slope/intercept mapping is the bridge between them. The same contract-versus- payload idea appears later with geometry, where metadata gives an array a location in patient space; this unit does not need to repeat that spatial derivation. It is enough to recognise that correct intensity decoding is one part of a larger image contract.
Signed vs unsigned: the other half of the trap
RescaleIntercept only does its job after the pixel bit pattern has been decoded
with the correct dtype. Two more tags describe that interpretation:
PixelRepresentation(0028,0103)—0= unsigned,1= signed.BitsAllocated(0028,0100)—16for classic CT Image Storage.
These fields are only part of the raw-decoding contract. Reading PixelData
directly also requires the applicable Transfer Syntax and byte order, as well as
BitsAllocated, BitsStored, HighBit, and PixelRepresentation. A
conditional example with unsigned uint16 samples and
RescaleIntercept = −1024 stores an air-like value as a small positive integer;
another conditional example with signed int16 samples and intercept 0 can
already encode negative values. These are examples, not assumptions about every
object. The preferred path is pydicom’s decoded pixel_array, which honours the
declared representation; if a uint16 array is reinterpreted as int16, values
below 32768 remain unchanged while upper-half values wrap by 65536, not by a
uniform shift. The DICOM pixel-encoding rules
define the relevant image-pixel fields.
Intensity decoding is therefore the first contract check, not the whole act of identifying an image. Before combining slices or comparing a derived object, a pipeline must know which DICOM object and series supplied the values and which instances belong together. The next section turns from the numerical payload to that organisational context: the Study, Series, and Instance hierarchy and the identifiers that keep apparently similar objects distinct. The separate spatial contract, including the geometry tags that assign an array to patient space, is developed in the geometry chapter.
© 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.