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 pydicomds = pydicom.dcmread("path/to/one_slice.dcm", force=True)
print(ds.SOPClassUID.name) # expect: CT Image Storageprint(ds.Modality) # CTprint(ds.PatientID, ds.StudyInstanceUID, ds.SeriesInstanceUID)print(ds.SOPInstanceUID) # this slice's identityprint(ds.Rows, ds.Columns, ds.BitsAllocated, ds.PixelRepresentation)print(ds.RescaleSlope, ds.RescaleIntercept)print(ds.PixelSpacing) # row\col, as decimal stringsprint(ds.ImagePositionPatient) # x,y,z of first voxel centreprint(ds.ImageOrientationPatient) # row-cosines\col-cosinesprint(ds.SliceThickness, getattr(ds, "SpacingBetweenSlices", None))print(ds.ConvolutionKernel)Before moving on, answer from the printout:
- Is
PixelDatasigned or unsigned? Look atPixelRepresentation(0= unsigned,1= signed) — this is the dtype half of the lesson 2 trap. - What is
RescaleIntercept? Does0 + interceptland near −1000 (air)? - Which UID would a segmentation object cite to say “I belong to this series”?
(Answer:
SeriesInstanceUIDfor the series,SOPInstanceUIDper source slice — see lesson 5.)
Task 2 — convert stored pixels to HU
import numpy as nparr = 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
ImagePositionPatientvalues 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
ImageOrientationPatientandPixelSpacingidentical 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
pydicomtold 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
- 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. - HU conversion is one line and one sanity check (
hu.min() ≈ −1000); skip it and every threshold, window and feature is silently wrong. - 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. - Slicer and
pydicomare 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.