From shadows to slices
A CT scanner never photographs the inside of anything. It records shadows from hundreds of directions, then a computer works backwards to the one object that could have cast all of them. This page follows that journey, from X-ray photon to the DICOM file you segment.
1The whole pipeline on one page
Four stages, from photon to file. Everything later on this page zooms into one of them.
Before diving in, it helps to see the whole route. A clinical CT scanner, a dental cone-beam (CBCT) unit and a laboratory microCT look very different, but they all run the same four stages. Hardware turns photons into numbers. Preprocessing turns those numbers into line integrals, sorted into a sinogram. Reconstruction turns the sinogram into a map of how strongly each point attenuates X-rays. Packaging rescales that map and writes it into DICOM.
Hardware
- X-ray tube emits photonsvoltage (kVp) sets their energy; current (mA) sets how many
- The object absorbs some of thembone more than fat, metal far more than bone
- A detector counts the survivorsscintillator turns X-rays into light, photodiodes into charge
Preprocessing
- Raw frames, one per anglecorrected with dark and flat (empty-beam) frames
- Take a logarithmconverts “fraction survived” into “total attenuation along the ray”
- Sort by angle: the sinogramone row per angle, one column per detector position
Reconstruction
- Filter each projectionthe “reconstruction kernel”: sharp or soft
- Backprojectsmear each filtered shadow back across the image
- Result: attenuation map μ(x, y, z)or, iteratively, refine a guess until it reproduces the data
Packaging
- Rescale to Hounsfield unitswater = 0, air = −1000
- Store as integers + metadataslope, intercept, spacing, position, orientation
- Write DICOM filesusually one file per slice, grouped in a series
Notice where the word “projection” lives. The physical scanner projects the object into shadows: that produces the sinogram. The computer backprojects those shadows into an image: that produces the slices. So the DICOM volume is not a projection of the sinogram; it is the result of undoing the projection. Chapters 3 and 4 make that precise.
The DICOM you receive almost always contains stages 9–12 only. The raw projections and the sinogram stay inside the scanner, in vendor formats. Every decision made in stages 4–10 (kernel, slice thickness, corrections) is baked into your voxels, and a few of them are recorded in the DICOM header. Chapter 8 shows where.
2One ray: why scanners take a logarithm
The only thing a CT detector ever learns is how many photons made it through. Everything starts from that single number per ray.
Picture a thin pencil of X-ray photons crossing the body. Each photon travels in a straight line until it is either absorbed or scattered away, and each centimetre of material has a fixed probability of stopping it. That probability depends on what the material is (its density and atomic number) and on the photon energy. Bone stops more photons per centimetre than soft tissue, and metal stops far more than bone.
“A fixed fraction lost per centimetre” is the recipe for exponential decay. If 17% of photons are stopped in the first centimetre of tissue, 17% of the remaining ones are stopped in the next, and so on. Run the beam below and watch the photons thin out as they pass each slab.
Two observations matter for everything that follows. First, the slabs multiply: the fraction surviving the whole path is the product of the fractions surviving each slab. Second, the detector cannot tell you where along the ray the absorption happened. Swap the slabs’ order and the reading is identical. One ray gives one number, and that number mixes everything along the path.
A product is awkward to work with, but its logarithm is a sum. That is why every CT system takes \(-\ln(I/I_0)\) of each reading: it turns “fraction of photons that survived” into “total attenuation accumulated along the ray”, which is a plain sum over the path. Reconstruction algorithms are designed to unscramble sums, not products.
Let \(\mu(x)\) be the linear attenuation coefficient (units: cm⁻¹), the probability per unit length that a photon is removed. Along a small step \(dx\), the intensity drops in proportion to itself:
\[ \frac{dI}{dx} = -\mu(x)\, I(x) \quad\Longrightarrow\quad I = I_0 \exp\!\Big(-\int_{\text{ray}} \mu(x)\, dx\Big) \]This is the Beer–Lambert law. Taking the log gives the quantity a CT scanner actually works with, the projection value \(p\):
\[ p \;=\; -\ln\frac{I}{I_0} \;=\; \int_{\text{ray}} \mu(x)\, dx \;\approx\; \sum_i \mu_i\, \Delta x_i \]In practice \(I_0\) is not known per pixel, so the detector is calibrated with a dark frame \(D\) (beam off) and a flat frame \(F\) (beam on, nothing in it):
\[ \frac{I}{I_0} \approx \frac{I_{\text{raw}} - D}{F - D} \]One caveat that will come back in chapter 7: \(\mu\) depends on photon energy \(E\), and a real tube emits a spectrum \(S(E)\), not one energy. The honest model is \(I = \int S(E)\, e^{-\int \mu(x,E)\,dx}\, dE\), whose logarithm is not exactly a sum. The resulting error is called beam hardening.
Each voxel value in your volume is an estimate of \(\mu\) at that point, later rescaled. Because \(\mu\) depends on energy, the same tissue scanned at 80 kVp and 140 kVp gives different raw \(\mu\). Hounsfield units (chapter 6) exist to cancel most of that difference.
3Many rays, many angles: the sinogram
One shadow is ambiguous. A shadow from every direction is not.
Now line up many parallel rays side by side, all hitting a row of detector elements. One exposure gives a whole projection: a 1D profile of total attenuation, like the shadow of the slice on a wall. Then the source and detector rotate a little and take another projection, and another, until they have gone half or all the way around.
Stack those profiles as rows of an image, one row per angle, and you get the sinogram. It is literally the raw data of CT. It looks nothing like the patient, and that is expected: the horizontal axis is “position on the detector” and the vertical axis is “angle of the gantry”.
Real scanners use a fan or a cone of rays from a single source rather than parallel rays. Chapter 5 shows that this only changes the bookkeeping, so we start with the parallel case where the geometry is cleanest.
Here is the origin of the name. Follow a single point while the scanner turns. When the rays are perpendicular to the line joining the point to the rotation centre, its shadow falls far to one side of the detector; half a turn later it falls on the other side; in between it sweeps across. Its shadow’s position traces a sine wave. The amplitude of that wave is the point’s distance from the centre, and its phase is the point’s angular position. A point exactly at the centre draws a straight vertical line.
A real slice is a continuum of points, each drawing its own sine wave with a brightness proportional to its attenuation. The sinogram is the sum of all those waves stacked on top of each other. Reconstruction asks the reverse question: which set of sine waves, with which brightness, adds up to exactly this picture?
Describe the slice by a function \(f(x,y)=\mu(x,y)\). A ray is a line at angle \(\theta\) and signed distance \(s\) from the centre: all points with \(x\cos\theta + y\sin\theta = s\). The projection value is the integral of \(f\) along that line. Collecting it for every \((\theta, s)\) defines the Radon transform (Johann Radon, 1917):
\[ p(\theta, s) \;=\; \mathcal{R}f\,(\theta,s) \;=\; \iint f(x,y)\;\delta\!\big(x\cos\theta + y\sin\theta - s\big)\;dx\,dy \]The sinogram is \(p(\theta,s)\) sampled on a grid. For a single point \(f=\delta(x-x_0)\,\delta(y-y_0)\), the delta function picks out only
\[ s(\theta) = x_0\cos\theta + y_0\sin\theta = r_0\cos(\theta-\varphi_0) \]with \(r_0\) and \(\varphi_0\) the point’s polar coordinates: a sinusoid of amplitude \(r_0\) and phase \(\varphi_0\). Because \(\mathcal{R}\) is linear, a whole slice gives a weighted sum of such sinusoids.
With parallel rays, 180° is enough, because the ray at \(\theta+\pi\) is the same line traversed backwards: \(p(\theta+\pi, s) = p(\theta, -s)\).
4Backprojection: running the shadows in reverse
This is the step you heard about. The scanner projects; reconstruction backprojects, after a crucial filtering step.
Take one detector reading. You know the total attenuation along its ray, but not where along the ray it came from. The most neutral guess is to spread it evenly along the whole line. Do that for every detector element of one projection and you get a smear: the shadow, stretched back across the image in the direction the rays came from.
Now do the same for every angle and add the smears up. Where the object really is, smears from all directions overlap and pile up. Elsewhere they mostly scatter. The object emerges. That is backprojection.
Try it below. Start with the filter set to “None”, one angle, then increase the number of angles.
With “None”, even 180 angles give a blurry, washed-out slice: every point is surrounded by a halo that fades with distance. The reason is a sampling imbalance. Every smear passes through the centre of whatever it hits, so each point collects some signal from all its neighbours, and the closer a neighbour, the more of its smears overlap. Fine detail (high spatial frequencies) is under-represented; coarse blobs (low frequencies) are over-represented.
The fix is to sharpen each projection before smearing it. The filter keeps every bump but adds small negative dips on either side of it (look at the blue curve). When the filtered projections are smeared back, those negative dips land exactly where the halos of neighbouring smears would be, and cancel them. The result is filtered backprojection (FBP), the workhorse of CT for fifty years.
Different filters trade sharpness against noise. The pure ramp gives the sharpest edges but amplifies noise the most; Shepp-Logan and Hann roll off the highest frequencies to produce smoother images. In a clinical scanner these choices are called reconstruction kernels, with names like “soft”, “standard”, “bone” or “lung”.
Why the filter is a ramp: the Fourier slice theorem
The filter is not a heuristic. It comes out of one striking fact, best seen as a picture. Take one projection and compute its 1D Fourier transform. The result is exactly one line through the centre of the 2D Fourier transform of the slice, at the same angle. So each angle hands you one spoke of the slice’s 2D spectrum, and all angles together fill the spectrum like the spokes of a wheel.
Each projection fills one spoke of the 2D spectrum. Near the centre (low frequencies) the spokes crowd together; far out (high frequencies) they spread apart. Plain backprojection counts every spoke equally, so low frequencies are over-weighted: that is the blur. Multiplying each spoke by its distance from the centre, \(|\omega|\), evens out the density. That multiplication is the ramp filter.
Backprojection is the adjoint of the Radon transform: each point \((x,y)\) collects the value of every ray that passed through it.
\[ b(x,y) \;=\; \mathcal{R}^{*}p\,(x,y) \;=\; \int_0^{\pi} p\big(\theta,\; x\cos\theta + y\sin\theta\big)\, d\theta \]Unfiltered, this returns \(b = f \ast \frac{1}{r}\): the true slice convolved with a \(1/r\) halo, the blur you saw. Fourier slice theorem: with \(P_\theta(\omega)\) the 1D Fourier transform of \(p(\theta,\cdot)\) and \(F\) the 2D transform of \(f\),
\[ P_\theta(\omega) \;=\; F(\omega\cos\theta,\; \omega\sin\theta) \]Write the inverse 2D Fourier transform of \(F\) in polar coordinates. The Jacobian of that change of variables is \(|\omega|\), and that is where the ramp comes from:
\[ f(x,y) \;=\; \int_0^{\pi}\!\! \int_{-\infty}^{\infty} P_\theta(\omega)\,|\omega|\, e^{\,i2\pi\omega s}\,d\omega\, d\theta, \qquad s = x\cos\theta + y\sin\theta \]Read it inside out: (1) Fourier-transform each projection, (2) multiply by \(|\omega|\), (3) transform back to get the filtered projection \(q_\theta = p_\theta \ast h\), (4) backproject. In discrete form with \(K\) angles:
\[ f(x,y) \;\approx\; \frac{\pi}{K}\sum_{k=1}^{K} q_{\theta_k}\!\big(x\cos\theta_k + y\sin\theta_k\big), \qquad Q_\theta(\omega) = |\omega|\,W(\omega)\,P_\theta(\omega) \]The window \(W(\omega)\) is the kernel choice: \(W=1\) is Ram-Lak; \(W=\operatorname{sinc}(\omega/2\omega_{\max})\) is Shepp-Logan; \(W=\tfrac12\big(1+\cos(\pi\omega/\omega_{\max})\big)\) is Hann. As a rule of thumb, an image \(N\) pixels wide needs about \(\tfrac{\pi}{2}N\) angles to avoid streaks; a clinical scanner records roughly 1000–2000 projections per rotation, a microCT 900–3600.
- The kernel is recorded in the DICOM tag
(0018,1210) Convolution Kernel. A sharp kernel gives crisp edges but more noise and slight overshoot at boundaries; a soft kernel gives smooth regions but blurred edges. The same scan is often reconstructed twice, so check which series you are segmenting. - Thresholds tuned on a soft-kernel series will behave differently on a sharp-kernel series of the same patient: noise widens every tissue’s histogram peak.
5Three machines, one idea: CT, CBCT and microCT
Real scanners use a single point-like source, so rays fan out. What changes between machines is the shape of that fan, what rotates, and how big the voxels end up.
An X-ray tube is close to a point source: photons leave a tiny focal spot and spread out. In a clinical scanner the beam is collimated into a thin fan that hits a curved strip of detector rows. In CBCT and microCT, the beam is a full cone that hits a flat panel detector, covering a whole volume at once. A fan-beam ray at angle γ inside a fan whose centre points at angle β is the same line as a parallel-beam ray at angle β + γ. So fan data can be re-sorted (“rebinned”) into a parallel sinogram, and everything from chapter 4 applies.
MicroCT: why the voxel size depends on the sample size
In a cone beam, an object close to the source casts a large shadow and one close to the detector casts a shadow of its own size. MicroCT exploits this as a magnifying glass. Slide the sample below and watch the trade-off: more magnification means smaller voxels, but also a smaller field of view, and a larger blur from the finite focal spot.
| Clinical CT | Dental / ENT CBCT | MicroCT (lab) | |
|---|---|---|---|
| What moves | Tube + detector spin (0.25–1 s per turn); table slides | Tube + panel do one slow turn (5–40 s) | Sample rotates; tube and detector fixed |
| Beam / detector | Fan, curved multi-row detector | Cone, flat panel | Cone, flat panel, microfocus tube |
| Projections | ~1000–2000 per turn | ~150–600 | ~900–3600, often averaged frames |
| Typical voxel | 0.3–1 mm in-plane, 0.5–5 mm slices | 0.075–0.4 mm, isotropic | 1–100 µm, isotropic |
| Reconstruction | Rebinning + FBP, or iterative / deep-learning | FDK, sometimes iterative | FDK (vendor software), sometimes iterative |
| Gray values | Calibrated Hounsfield units | Not reliable HU, vary with position and scan | Arbitrary or μ in mm⁻¹; density needs a phantom |
| Usual output | DICOM, one file per slice | DICOM, often one multi-frame file | Often TIFF / BMP stacks + log file; DICOM optional |
Fan → parallel rebinning. A fan with source angle \(\beta\), source-to-centre distance \(R\) and ray angle \(\gamma\) inside the fan contains the parallel ray with
\[ \theta = \beta + \gamma, \qquad s = R\sin\gamma \]Magnification (microCT, CBCT), with SOD the source–object and SDD the source–detector distances, detector pixel pitch \(d\) and focal spot size \(a\):
\[ M = \frac{\text{SDD}}{\text{SOD}}, \qquad \text{voxel} \approx \frac{d}{M}, \qquad \text{focal blur (object scale)} \approx a\,\frac{M-1}{M} \]FDK (Feldkamp–Davis–Kress, 1984), the standard cone-beam algorithm, is FBP with two geometric weights. For a flat detector with coordinates \((u,v)\) rescaled to the rotation axis, source distance \(D\) and source angle \(\beta\):
\[ f(\mathbf{x}) \approx \frac12\int_0^{2\pi} \frac{D^2}{U(\mathbf{x},\beta)^2}\; \Big[\Big(\tfrac{D}{\sqrt{D^2+u^2+v^2}}\;p_\beta\Big) \ast_u h\Big]\big(u(\mathbf{x},\beta),\,v(\mathbf{x},\beta)\big)\; d\beta \]Three steps: weight each projection by the cosine of the ray angle, ramp-filter each detector row, then backproject along the diverging rays with a distance weight \(D^2/U^2\) (\(U\) is the distance from the source to \(\mathbf{x}\) along the central ray). FDK is exact only in the central plane; slices far from it get “cone-beam artifacts”.
Iterative reconstruction abandons the inversion formula. It writes the scan as a big linear system: unknown voxels \(\mathbf{x}\), system matrix \(A\) (entry \(a_{ij}\) = length of ray \(i\) inside voxel \(j\)), measured line integrals \(\mathbf{b}\). Then it solves
\[ \hat{\mathbf{x}} = \arg\min_{\mathbf{x}\ge 0}\; \|A\mathbf{x}-\mathbf{b}\|^2_{W} + \lambda\, R(\mathbf{x}) \]where \(W\) down-weights noisy rays and \(R\) penalises noise (e.g. total variation). The simplest solver, SIRT, repeats “project the current guess, compare with the data, backproject the difference”:
\[ \mathbf{x}^{(n+1)} = \mathbf{x}^{(n)} + C\,A^{\!\top} R_w\big(\mathbf{b} - A\mathbf{x}^{(n)}\big) \]with \(C\) and \(R_w\) the inverse column and row sums of \(A\). Clinical products (ASiR, iDose, ADMIRE, AIDR…) and newer deep-learning reconstructions belong to this family. They lower noise at the same dose, but change the noise texture, which matters for texture-based analysis (radiomics).
6From μ to Hounsfield units to stored integers
Reconstruction gives attenuation in cm⁻¹. Your DICOM stores integers. Two small formulas sit in between, plus one display trick that is not data at all.
The raw output of FBP is \(\mu\), which depends on the tube voltage, the filtration and the detector. To make numbers comparable between scanners, Godfrey Hounsfield pinned the scale to two materials everyone has: water reads 0 and air reads −1000. Every other tissue lands on that ruler according to how much more or less it attenuates than water. Fat sits slightly below zero, muscle and blood slightly above, bone in the hundreds to over a thousand, and metal goes off the chart.
A monitor shows about 256 gray levels, but the ruler spans several thousand units. So viewers apply a window: they choose a range (centre and width), stretch it across the gray levels and clip everything outside to black or white. The window changes only what you see, never the stored numbers. Hover over the slice to read the values at each pixel, and try the narrow window to find the faint lesion near the spine.
Same pixel, three numbers. The scanner reconstructed \(\mu\). It converted \(\mu\) to HU. It stored HU + 1024 as an unsigned integer, and wrote RescaleSlope = 1, RescaleIntercept = −1024 in the header so you can undo it.
Hounsfield units, with \(\mu_w\) and \(\mu_{a}\) the attenuation of water and air measured on the same scanner at the same voltage:
\[ \text{HU} = 1000 \times \frac{\mu - \mu_{w}}{\mu_{w} - \mu_{a}} \;\approx\; 1000 \times \frac{\mu - \mu_w}{\mu_w} \]Storage. Pixels are written as integers (usually 12 bits stored in 16) with a linear map back to physical units:
\[ \text{HU} = \texttt{RescaleSlope} \times \text{SV} + \texttt{RescaleIntercept} \]Display window with centre \(L\) and width \(W\) (DICOM’s exact definition adds ±0.5 offsets):
\[ g = 255 \cdot \operatorname{clip}\!\Big(\frac{\text{HU} - (L - W/2)}{W},\; 0,\; 1\Big) \]| Material | Typical HU (CT, ~120 kVp) |
|---|---|
| Air | −1000 |
| Lung parenchyma | −900 to −500 |
| Fat | −120 to −80 |
| Water | 0 |
| Muscle, blood, organs | +20 to +60 |
| Iodinated contrast in vessels | +150 to +500 |
| Trabecular to cortical bone | +200 to +1900 |
| Metal | above +3000, often clipped at +3071 |
- Clinical CT: HU are calibrated daily on water, so fixed thresholds (e.g. bone above +250) transfer reasonably well between patients and scanners, within a few tens of HU.
- CBCT: gray values are not true HU even if the header says so. Scatter, the small field of view and truncation make the same tissue read differently depending on its position and on what else is in the beam. Prefer per-scan normalisation, histogram-based thresholds (e.g. Otsu) or learned models over fixed HU thresholds.
- MicroCT: values are usually raw \(\mu\) or arbitrary scaled units. Bone mineral density in mg HA/cm³ requires scanning hydroxyapatite calibration phantoms with the same settings and fitting a line from gray value to density.
- Always segment on the rescaled values (HU or calibrated units), never on an 8-bit windowed screenshot or PNG export: windowing throws away everything outside its range.
7Artifact lab: break the scanner yourself
Every artifact is a place where the real measurement stopped matching the simple model of chapters 2–4. Knowing the cause tells you what the artifact looks like in your volume.
FBP assumes four things: every ray is a perfect line integral, every detector element is calibrated, there is no noise, and the angles are dense enough. Below, each switch breaks one assumption on the simulated scanner, and the reconstruction pipeline from chapter 4 runs on the damaged data. The profile plot follows one row of the image (click the reconstruction to choose another row).
Two artifacts cannot be simulated in a single static slice but matter just as much for segmentation:
- Partial volume. A voxel straddling bone and soft tissue gets an intermediate value. Thin structures (trabeculae, cortical shells, small vessels) look less dense than they are and can vanish under a threshold, while thick slices blend neighbouring anatomy. This is a property of every voxel grid, not a fault.
- Motion. If the object moves during the rotation, projections disagree with each other, producing streaks, doubled edges and blur. Breathing and heartbeat in clinical CT, patient movement in long CBCT scans, and sample drying or shifting in long microCT scans.
Noise. Photon counts are Poisson: \(I \sim \text{Poisson}(I_0 e^{-p})\). After the log, the variance of a projection value is roughly \(\operatorname{Var}(p) \approx 1/\bar I = e^{p}/I_0\): rays through dense paths are far noisier. The ramp filter amplifies high frequencies, and image noise scales as \(\sigma \propto 1/\sqrt{\text{dose}}\).
Rings. A detector element with a gain error \(g\) adds a constant \(-\ln g\) to one column of the sinogram at every angle. Backprojected, a constant at fixed \(s=s_0\) becomes a circle of radius \(|s_0|\) around the rotation axis.
Beam hardening. With a spectrum, \(p_{\text{meas}} = -\ln \int S(E)\,e^{-\int\mu(x,E)\,dx}\,dE\) grows slower than linearly with path length, because low-energy photons are removed first and the surviving beam is “harder”. Modelled here as \(p_{\text{meas}} \approx p - \kappa p^2\). Long central paths are under-estimated, so the centre comes out darker (cupping), and dark streaks appear between dense objects.
Metal. Through metal, \(e^{-p}\) is so small that few photons survive (photon starvation), and beam hardening is extreme. Those rays are inconsistent with all the others, and the ramp filter spreads the inconsistency along their whole length: bright and dark streaks radiating from the metal.
Too few angles. The Fourier spokes from chapter 4 leave gaps far from the centre. FBP fills them with aliasing streaks. Iterative methods with a prior \(R(\mathbf{x})\) cope better; the lab uses total variation, \(R(\mathbf{x}) = \sum \|\nabla \mathbf{x}\|\), which favours flat regions with sharp borders.
8The DICOM file: what actually lands on your disk
A DICOM file is a list of labelled values followed by the pixels. A handful of those labels decide whether your volume is the right shape, the right size and in the right place.
DICOM organises data in a four-level hierarchy: a patient has studies (one visit), a study has series (one reconstruction: one kernel, one slice thickness, or one scout image), and a series has instances. In classic CT, each instance is one file holding one slice. Newer “enhanced” objects, common in CBCT, put every slice of the series in a single multi-frame file.
Inside a file, everything is a data element: a tag written as two hexadecimal numbers (group, element), a two-letter type code (the VR, e.g. DS for decimal string, US for unsigned short), a length, and the value. The pixels are just one more element, the last one, with tag (7FE0,0010).
The tags that matter for analysis
| Tag | Name | Why you care |
|---|---|---|
(0020,000E) | Series Instance UID | Groups files into one volume. Never mix series: a folder often holds several reconstructions plus scout images. |
(0020,0032) | Image Position (Patient) | 3D position in mm of the centre of the first transmitted pixel. Sort slices with it, not with file names. |
(0020,0037) | Image Orientation (Patient) | Two unit vectors: direction of rows and direction of columns in patient space. |
(0028,0030) | Pixel Spacing | In-plane spacing in mm, given as [row spacing, column spacing], i.e. vertical first. |
(0018,0050) | Slice Thickness | Nominal thickness of each reconstructed slice. Not the gap between slices: slices may overlap or have gaps. |
(0028,1052) / (0028,1053) | Rescale Intercept / Slope | Convert stored integers to HU (or other units). Apply before any thresholding. |
(0028,0100) / (0028,0103) | Bits Allocated / Pixel Representation | 16-bit, signed (1) or unsigned (0). Reading signed data as unsigned turns −1 into 65535. |
(0028,0120) | Pixel Padding Value | Value used outside the circular reconstruction area (often −2000 or −3024). Mask it out of statistics. |
(0028,1050) / (0028,1051) | Window Center / Width | Suggested display window only. Ignore for analysis. |
(0018,1210) | Convolution Kernel | The reconstruction filter from chapter 4. Affects noise and edge sharpness. |
(0018,0060) / (0018,1151) | KVP / X-Ray Tube Current | Beam energy and dose: affect contrast, HU of dense materials and noise. |
(0018,1120) | Gantry/Detector Tilt | If non-zero, slices are sheared relative to each other; positions already account for it, but naive stacking does not. |
(0008,0060) | Modality | CT for clinical CT, and frequently for CBCT too. Check the manufacturer and model tags to tell them apart. |
From pixel indices to millimetres
Each slice is a flat sheet floating in a patient-attached coordinate system called LPS: x increases towards the patient’s left, y towards the back (posterior), z towards the head (superior). The header tells you where the sheet’s first pixel sits and which way its rows and columns point. Everything else follows.
With \(\mathbf{S}\) = Image Position, \(\mathbf{X}\) = first three values of Image Orientation, \(\mathbf{Y}\) = last three, \(\Delta_r, \Delta_c\) = Pixel Spacing (row spacing first), the centre of the pixel at row \(r\), column \(c\) is at
\[ \mathbf{P}(r,c) = \mathbf{S} + c\,\Delta_c\,\mathbf{X} + r\,\Delta_r\,\mathbf{Y} \]Slices are ordered by their position along the normal \(\mathbf{n} = \mathbf{X}\times\mathbf{Y}\), and the true slice spacing is the difference of consecutive \(\mathbf{n}\cdot\mathbf{S}_k\). As a 4×4 affine (the form used by ITK, nibabel and friends):
\[ \begin{pmatrix}\mathbf{P}\\1\end{pmatrix} = \begin{pmatrix} \Delta_c\mathbf{X} & \Delta_r\mathbf{Y} & \Delta_k\mathbf{n} & \mathbf{S}_0\\ 0 & 0 & 0 & 1\end{pmatrix} \begin{pmatrix} c\\ r\\ k\\ 1\end{pmatrix} \]Watch the conventions: DICOM is LPS, while NIfTI, 3D Slicer and nibabel use RAS, which flips the signs of x and y. NumPy arrays are usually indexed \([k, r, c]\), the reverse of \((x,y,z)\).
Loading a CT series correctly in Python
import glob, numpy as np, pydicom dsets = [pydicom.dcmread(f) for f in glob.glob("series_dir/*")] uid = dsets[0].SeriesInstanceUID dsets = [d for d in dsets if d.SeriesInstanceUID == uid] # one series only # sort along the slice normal, not by file name or InstanceNumber X, Y = np.array(dsets[0].ImageOrientationPatient, float).reshape(2, 3) n = np.cross(X, Y) pos = lambda d: float(np.dot(n, np.array(d.ImagePositionPatient, float))) dsets.sort(key=pos) # stored integers -> HU vol = np.stack([d.pixel_array.astype(np.float32) * float(getattr(d, "RescaleSlope", 1)) + float(getattr(d, "RescaleIntercept", 0)) for d in dsets]) # voxel spacing (k, r, c) in mm; SliceThickness is NOT the spacing dk = np.median(np.diff([pos(d) for d in dsets])) dr, dc = map(float, dsets[0].PixelSpacing) # [row, column] print(vol.shape, (dk, dr, dc), dsets[0].get("ConvolutionKernel")) # Shortcut that does all of the above (and keeps LPS geometry): # import SimpleITK as sitk; r = sitk.ImageSeriesReader() # r.SetFileNames(r.GetGDCMSeriesFileNames("series_dir")); img = r.Execute()
- One series at a time; drop scouts and localizers (different orientation, a single slice).
- Sort by position along the normal; check that the spacing is uniform (missing slices happen).
- Apply slope and intercept, respecting signed or unsigned storage; mask the padding value.
- Record spacing, kernel, slice thickness, kVp and contrast agent (
(0018,0010)) as metadata for your results. - CT: HU thresholds are meaningful. CBCT and microCT: normalise or calibrate per scan first.
- Expect artifacts near metal, at the field-of-view edge and far from the central plane in cone-beam data; exclude or flag those regions.
- Resample to isotropic voxels only if your method needs it, and keep the original spacing for measurements.