Skip to content

Open one real CT and read its contract

Tonight · ~40 min · hands-on · energy: active · setup: Python (pydicom, numpy) + one RIDER CT slice, optional 3D Slicer

Everything so far has been mental models. Now you hold one real DICOM object and read its contract directly. The goal is not to memorise tags — it is to build the reflex of reading the metadata before trusting the pixels.

The anchor: trust neither the array nor the viewer blindly

You will load one slice, print its contract, convert stored pixels to HU, and then cross-check against 3D Slicer. If the two disagree, you do not pick a winner — you find the tag that differs. That hunt is exactly the geometry-mismatch diagnosis Chapter 2 teaches you to do systematically.

Data

Use a RIDER Lung CT v3 case — TRACE-CT already downloads and preflights these. RIDER is a public TCIA test–retest lung CT dataset, ideal because it has matched repeat scans and tumour segmentations that later chapters reuse. See the TRACE-CT learning map for the exact local path. If you do not have it yet, the same steps work on any single-file CT DICOM from a public TCIA series.

Never commit patient data to this repo — work under a gitignored data/ path (the data boundary is enforced mechanically by .gitignore).

Task 1 — read the contract

import pydicom
ds = pydicom.dcmread("path/to/one_slice.dcm", force=True)
print(ds.SOPClassUID.name) # expect: CT Image Storage
print(ds.Modality) # CT
print(ds.PatientID, ds.StudyInstanceUID, ds.SeriesInstanceUID)
print(ds.SOPInstanceUID) # this slice's identity
print(ds.Rows, ds.Columns, ds.BitsAllocated, ds.PixelRepresentation)
print(ds.RescaleSlope, ds.RescaleIntercept)
print(ds.PixelSpacing) # row\col, as decimal strings
print(ds.ImagePositionPatient) # x,y,z of first voxel centre
print(ds.ImageOrientationPatient) # row-cosines\col-cosines
print(ds.SliceThickness, getattr(ds, "SpacingBetweenSlices", None))
print(ds.ConvolutionKernel)

Before moving on, answer from the printout:

  • Is PixelData signed or unsigned? Look at PixelRepresentation (0 = unsigned, 1 = signed) — this is the dtype half of the lesson 2 trap.
  • What is RescaleIntercept? Does 0 + intercept land near −1000 (air)?
  • Which UID would a segmentation object cite to say “I belong to this series”? (Answer: SeriesInstanceUID for the series, SOPInstanceUID per source slice — see lesson 5.)

Task 2 — convert stored pixels to HU

import numpy as np
arr = ds.pixel_array.astype(np.float64)
hu = arr * float(ds.RescaleSlope) + float(ds.RescaleIntercept)
print("min/max HU:", hu.min(), hu.max()) # expect min ≈ -1000 (air)

Sanity-check: air (outside the body) should read ≈ −1000 HU; a soft-tissue region should sit near 0 (±a few tens). If hu.min() is near 0 or a large positive number, you have used stored pixels as HU — re-read lesson 2.

Bridge

When hu.min() ≈ −1000, you have demonstrated that the array holds encoded intensity and that the physical meaning lives in RescaleSlope/RescaleIntercept. That is the contract-vs-payload split, made real on data you control.

Task 3 — compare two adjacent slices

Load two neighbouring slices from the same series. Compare:

  • Are the ImagePositionPatient values different only along the direction of the slice normal? (Compute the normal as the cross product of the row and column cosines — Chapter 2 does this in detail.)
  • Are ImageOrientationPatient and PixelSpacing identical between the two? They should be within one regular series.
  • How far apart are the slice centres along the normal? That distance is your slice spacing — compare it to SpacingBetweenSlices / SliceThickness.

This is the first time you measure spacing from geometry rather than trusting a tag. It is the seed of Chapter 2’s slice-ordering lesson.

Task 4 — cross-check against 3D Slicer

Drag the whole series folder into 3D Slicer (it groups by SeriesInstanceUID). Then:

  • In Volumes, read the displayed spacing and origin. They should match what pydicom told you.
  • Apply a lung window (W≈1500, L≈−600) and confirm the parenchyma looks right.
  • Use the DICOM metadata viewer to find the same tags you read in Task 1.

If Slicer and your pydicom readout disagree, trust neither blindly — find the tag that differs. That is precisely the kind of geometry mismatch Chapter 2 teaches you to diagnose.

For the visual side of Slicer, the 3D Slicer companion maps each verified Witowski lesson to a concept plus a small RIDER exercise. Use it rather than rebuilding a generic tutorial.

Task 5 — flag the geometry tags for Chapter 2

Before closing, write down the five tags that will later determine whether this series is a safe, regular 3D volume: ImagePositionPatient, ImageOrientationPatient, PixelSpacing, SliceThickness, and FrameOfReferenceUID. You will need all five in the next chapter.

What to retain

  1. The reflex this lab builds: print the contract, then trust the pixels. RescaleSlope/Intercept, PixelRepresentation, the UIDs, and the geometry tags come before any array operation.
  2. HU conversion is one line and one sanity check (hu.min() ≈ −1000); skip it and every threshold, window and feature is silently wrong.
  3. Spacing measured from geometry (the distance between adjacent slice positions along the normal) is the seed of all of Chapter 2 — trust it over filenames and over InstanceNumber.
  4. Slicer and pydicom are two readers of the same contract; when they disagree, the tag that differs is the diagnosis.

You have finished Chapter 1. For the dense tag table, the VR/transfer-syntax reference, the SOP-class list, and the full failure-mode catalogue, see the CT & DICOM reference. Then continue to Chapter 2 — Geometry, segmentation and resampling.