Skip to main content

radreader

Read 2-D X-ray (CR / DX) and mammography (MG) DICOMs as the image a radiologist would see. Pure pydicom, following the DICOM order: rescale → window / VOI LUT → MONOCHROME1 flip → padding. radreader is also a quality checker and a fixer for broken headers.

import radreader

img = radreader.read("chest.dcm")
img.pixels            # float32 (rows, cols) in [0, 1], no transpose needed
img.meta["voi"]       # {'source': 'window', 'center': 2048.0, 'width': 4096.0, 'function': 'LINEAR', ...}
img.issues            # [Issue(id='HDR_HIGHBIT_MISSING', severity='info', fixed_by='highbit_from_bits_stored', ...)]

Install

pip install radreader                 # numpy, pydicom, pylibjpeg + libjpeg (JPEG Lossless), python-gdcm
pip install "radreader[pylibjpeg]"    # + pylibjpeg-openjpeg (JPEG 2000)
pip install "radreader[torch]"        # radreader.torch.to_tensor
pip install "radreader[monai]"        # radreader.monai.RadReader

For development: uv sync --all-extras, then .venv/bin/python -m pytest tests/.

Reading

radreader.read(path, voi=True, voi_source="auto", window_index=0, window=None,
               out_range=(0.0, 1.0), qc="on", fix="header", fixers=None)
Argument Meaning
voi Apply the window / VOI LUT. False: modality units (slope * raw + intercept), MONOCHROME1 still flipped, out_range ignored
voi_source "auto" (VOI LUT if present, else window), "window" or "lut"
window_index Which of several windows / LUTs (default 0). The standard calls them alternative views and does not name a main one
window "SOFTER" (by WindowCenterWidthExplanation or LUTExplanation), or (center, width) / (center, width, "SIGMOID")
out_range Output range, e.g. (-1, 1)
qc "on" (header + pixel checks), "off" (skip checks; header fixes still run), "strict" (raise on warn issues)
fix "header" (always on) or "image" (also image fixers, e.g. a percentile window for a clipped image)
fixers Your own fixer functions, tried before the built-in ones

path can be a file path or a binary file object. meta holds the technical tags and the choices made, including photometric, transfer_syntax, decoder, windows (all options), voi (the one used), pixel_spacing (PixelSpacing, else ImagerPixelSpacing) with pixel_spacing_source, geometry, view, image_laterality, body_part, presentation_intent, vendor (cleaned, e.g. "GE Healthcare" → GE), padding_mask, steps and issues. No patient tags.

Errors: InvalidDicomError (not DICOM, partial download), UnsupportedImageError (no pixels, multi-frame, color), DecoderMissingError (with the install command), DecodeError. All are RadReaderError, with the step, transfer syntax and file in the message and the original exception chained.

What it handles

Case What radreader does
HighBit missing BitsStored - 1 (ITK 5.4.5 assumes 0 and loads the image blank)
MONOCHROME1 with a window Window on the stored values, then flip. Flipping first picks the wrong pixels (the RadHarmony / ITK bug)
MONOCHROME1 flip 1 - x on the fixed output range, never image min / max
No window Full possible range from BitsStored and sign, through slope / intercept
Empty window tags (old Fuji CR) Treated as no window
Several windows / VOI LUTs (GE mammo and knee) Pick by index or name; meta["windows"] lists all
LINEAR, LINEAR_EXACT, SIGMOID DICOM PS3.3 C.11.2.1.2 formulas, output exactly in [0, 1]
VOI LUT with fewer data bits than declared (GE: bits 16, max 16382) Normalized by the real bit range, so it is not dark
LUT Descriptor length 0, LUTData as OW bytes 65536 entries, read as uint16
Decreasing VOI LUT (KODAK knee) Applied as the standard says; HDR_LUT_INVERTS notes that output without it is inverted
Window + VOI LUT both present VOI LUT by default (voi_source="auto"), window with voi_source="window"
Unusual rescale (slope 2.81525, intercept -240, RescaleType OD) Normal Modality LUT math
Padding value, padding range limit (Hologic / GE mammo) Padded pixels are black; meta["padding_mask"]
Raw values above BitsStored High bits ignored (PS3.5 8.1.1) and flagged; fix="image" re-reads with the real bit depth
MONOCHROME2 + PresentationLUTShape INVERSE Not flipped, flagged (0 files in the NAS scans)
FOR PROCESSING mammograms Read as usual, flagged, meta["presentation_intent"]
JPEG Lossless (97% of mammo / knee), JPEG 2000, RLE, JPEG-LS pydicom decoders; gdcm first (1.9x faster on JPEG Lossless, 2.8x on JPEG 2000, same output), pylibjpeg if it fails. radreader.set_decoder_preference([...])
Missing BitsStored, BitsAllocated, Rows / Columns, TransferSyntaxUID, PixelRepresentation, SamplesPerPixel Header fixers (see docs/qc_checks.md), chosen with the tag removal experiments
Missing PhotometricInterpretation PresentationLUTShape INVERSE -> MONOCHROME1, else MONOCHROME2 (warn)
Presentation States, SR in a folder Reported as "not an image" (FILE_NO_PIXELS), not as errors
Multi-frame (tomosynthesis) Rejected with a clear error (3-D is planned)

Spacing differs from ITK on purpose: radreader uses PixelSpacing (magnification corrected) first, ITK uses ImagerPixelSpacing for CR / DX / MG. Both raw values are in meta.

Quality checks

radreader.qc("x.dcm", level="header")        # list[Issue]; levels: file, header, pixel
rows = radreader.check("/data/folder", level="pixel", workers=16, out="report.csv")   # never stops on errors
issues, table = radreader.dataset_qc(rows)   # duplicates, mixed bit depth per vendor, intensity outliers
for r in radreader.scan("/data/folder"):     # headers only: ok / not_image / unsupported / error / hidden
    print(r.path, r.status)

The CSV report holds technical tags only (safe to share), plus SOPInstanceUID and a pixel hash for the duplicate checks. All issue ids: docs/qc_checks.md.

radreader qc /data/folder --level pixel --workers 16 --out report.csv   # summary by issue id x vendor
radreader fix in_folder out_folder --report changes.csv                   # repaired headers, pixels untouched

Fixers

Header fixers always run (the pixels need them) and are listed as issues with fixed_by. Image fixers are off by default, so the default output stays "what the DICOM says".

@radreader.fixer("PIX_LOW_CONTRAST", kind="image")
def my_clahe(pixels, ds, meta):
    ...
    return new_pixels

img = radreader.read(path, fixers=[my_clahe])      # or fix="image" for all registered image fixers
radreader.fix_file("in.dcm", "out.dcm")            # header fixes written to a new file; UIDs kept

After a fixer, its check runs again: pass → fixed_by is set; fail → the next fixer is tried. A fixer that raises is recorded as FIX_FAILED. Image fixers act on QC issues, so they do not run with qc="off".

PyTorch and MONAI

from radreader.torch import to_tensor
x = to_tensor(radreader.read(path))                 # (1, H, W) float32

import monai as mn
from radreader.monai import RadReader
load = mn.transforms.LoadImageD(keys="img", reader=RadReader(out_range=(-1, 1)))

RadReader gives (W, H) by default (swap_ij=True), like MONAI's ITKReader and PydicomReader, so it drops into a pipeline that already transposes. The 4x4 affine holds the spacing (DX / MG files usually have no position). It is picklable for DataLoader workers.

Speed

All pixel math (modality, VOI, flip, padding, output range) is one lookup table over every possible raw value, then out = lut[raw]. The table is cached by PixelParams. Each file is read once. Decoding is most of the time (about 75% for JPEG Lossless mammograms).

Design

Three stages, so 3-D can be added without a rewrite: params = parse(ds) (header → PixelParams), raw = decode(ds), pixels = render(raw, params) (any array shape). See docs/reading_steps.md and docs/plan_full.md.

License

Apache-2.0

Metadata

Release files for radreader 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for radreader 0.1.0
File Size Uploaded
radreader-0.1.0.tar.gz 55.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for radreader 0.1.0
File Interpreter ABI Platform
radreader-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 101.4 kB

Release files / radreader-0.1.0.tar.gz

Download URL radreader-0.1.0.tar.gz
Size 55.2 kB
Tags Source
SHA-256 checksum
How to use checksums
fba701998ffee1c931847369170ccf6b6c613991e339b0daeff9aaab6a85c34e
BLAKE2b-256 checksum
How to use checksums
ec24a48ed5ac034e4b54d6e816d9511f8101b7df19bf248e0d3263c62971ee38
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.1 {"installer":{"name":"uv","version":"0.10.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / radreader-0.1.0-py3-none-any.whl

Download URL radreader-0.1.0-py3-none-any.whl
Size 46.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
954da5d68fa0c958ca4ddfdcc03adb1df16353677ae432c25f4fa7128ae2646c
BLAKE2b-256 checksum
How to use checksums
ce6a195c23b19cf61e979197c714e93ecf889c8410df5b27138ace2ee256735b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.1 {"installer":{"name":"uv","version":"0.10.1","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page