DEV Community

kaveen
kaveen

Posted on

[OC] I built a free, open-source HIPAA DICOM anonymizer for medical imaging

I just released dicom-clean — an open-source CLI tool for HIPAA-compliant DICOM de-identification. Here's how it works and why I built it.

The Problem

Medical imaging files (CT scans, MRIs, X-rays) use a format called DICOM, which contains two things:

  1. Metadata — patient name, birth date, MRN, hospital, referring physician, etc.
  2. Pixel data — the actual image

When hospitals want to share DICOM data for research, AI training, or vendor evaluation, they must remove the patient identifiers first — this is required by HIPAA Safe Harbor (45 CFR §164.514(b)(2)).

But existing options are problematic:

  • Commercial tools (RSNA CTP, DicomCleaner) cost $5,000+/year
  • Cloud solutions upload patient data → HIPAA violation risk
  • DCMTK / GDCM work but have steep learning curves (C++ toolchain, complex syntax)

So I built dicom-clean to be:

  • ✅ 100% offline (air-gapped compatible)
  • ✅ Simple CLI (one command)
  • ✅ Zero-install (standalone .exe)
  • ✅ Free and open source (AGPL-3.0)

What It Does

1. Strips All 18 HIPAA Patient Identifiers

The tool blanks out 40+ scalar PHI tags and deletes 8 sequences that may carry nested PHI:

HIPAA_TAGS_TO_BLANK = (
    (0x0010, 0x0010),  # PatientName
    (0x0010, 0x0020),  # PatientID
    (0x0010, 0x0030),  # PatientBirthDate
    (0x0010, 0x1040),  # PatientAddress
    (0x0008, 0x0080),  # InstitutionName
    (0x0008, 0x0090),  # ReferringPhysicianName
    # ... 40+ more
)

HIPAA_TAGS_TO_REMOVE = (
    (0x0008, 0x1110),  # ReferencedStudySequence
    (0x0008, 0x1115),  # ReferencedSeriesSequence
    # ... 6 more
)
Enter fullscreen mode Exit fullscreen mode

Design decision: I blank the scalar tags (preserving tag + VR) rather than deleting them, so downstream PACS and DICOM viewers don't break when they expect those fields.

2. Deterministic UID Pseudonymization

DICOM uses UIDs (Unique Identifiers) to link files together. A study has a StudyInstanceUID, each series has a SeriesInstanceUID, and each image has a SOPInstanceUID.

If you just replace these with random UUIDs, you lose the relationships between files — a PACS server won't be able to group images from the same study.

My solution: SHA-256 with an optional salt.

def pseudonymize_uid(original_uid: str, salt: str = "") -> str:
    """Deterministically pseudonymize a DICOM UID."""
    if not original_uid:
        return original_uid
    digest = hashlib.sha256(
        (salt + str(original_uid)).encode("utf-8")
    ).hexdigest()
    number = int(digest[:32], 16)  # 128 bits
    return f"2.25.{number}"
Enter fullscreen mode Exit fullscreen mode

Why this works:

  • Deterministic — Same input + salt → same output, always
  • Reversible relationships — All files from one study get consistent pseudonyms
  • Non-reversible — Without the salt, you can't reverse it
  • Valid format — Output is a proper DICOM UID (2.25.<128-bit-int>)

3. Image Integrity

The pixel data is never touched. Same for:

  • Modality (CT, MR, US, etc.)
  • PixelData
  • TransferSyntaxUID
  • PhotometricInterpretation
  • Rows / Columns
  • BitsAllocated / BitsStored
  • PixelSpacing / SliceThickness
  • Window/Level tags

So the image looks identical in any DICOM viewer — only the metadata changes.

4. Date Reduction

HIPAA permits keeping the year but not the month/day. So:

Before: StudyDate = "20250315"
After:  StudyDate = "20250101"
Enter fullscreen mode Exit fullscreen mode

Tech Stack

Component Purpose
pydicom DICOM parsing & writing
Typer CLI framework
Rich Terminal UI (panels, progress bars, tables)
PyInstaller Standalone .exe build

What It Looks Like

$ dicom-clean sanitize patient.dcm output\ --salt "my-secret"
Enter fullscreen mode Exit fullscreen mode

The output shows a live progress bar and a colored summary table:

  • Total Files Scanned: 1,250
  • Successfully Anonymized: 1,248
  • Skipped (Non-DICOM): 2
  • Errors / Failed: 0

Directory Batch Processing

The tool recursively processes directories and preserves the original subfolder structure:

$ dicom-clean sanitize C:\Hospital_Data C:\Clean_Output --salt "hospital-2025"
Enter fullscreen mode Exit fullscreen mode

Robustness

  • Non-DICOM files are skipped with a warning instead of crashing
  • Per-file errors are collected into a summary
  • Uses try/except for InvalidDicomError, OSError, IOError, and generic exceptions
  • Exit code 1 if any file failed, 0 otherwise

Installation

Windows Standalone (No Python Required)

Download dicom-clean.exe from the Releases page and run:

dicom-clean.exe sanitize input.dcm output\ --salt "my-secret"
Enter fullscreen mode Exit fullscreen mode

From Source

git clone https://github.com/kaveen-ai/dicom-clean.git
cd dicom-clean
pip install -r requirements.txt
python anonymizer.py --help
Enter fullscreen mode Exit fullscreen mode

Design Tradeoffs

Some decisions I made and why:

1. SHA-256 hashing vs random UUIDs for UIDs

  • ✅ Hash preserves relationships across files
  • ✅ Deterministic (reproducible)
  • ❌ Requires salt management for security

2. Blank tags vs delete tags

  • ✅ Blanking preserves tag structure (PACS-safe)
  • ✅ Downstream tools don't break
  • ❌ Leaves an empty tag with metadata footprint

3. AGPL-3.0 + Commercial dual-license

  • ✅ Free for open-source use
  • ✅ Commercial license funds development
  • ❌ AGPL can be a dealbreaker for some hospitals

4. Single-file CLI vs library-first

  • ✅ Standalone .exe for non-technical users
  • ✅ The core DicomAnonymizer class is importable as a library
  • ✅ CLI is just a thin Typer wrapper

The DicomAnonymizer Class

If you want to use the engine programmatically:

from anonymizer import DicomAnonymizer

anonymizer = DicomAnonymizer(
    salt="my-hospital-secret",
    remove_private_tags=True,
    reduce_dates_to_year=True,
)

summary = anonymizer.anonymize(
    input_path="patient.dcm",
    output_path="output/",
)

print(f"Anonymized {summary.anonymized} files")
Enter fullscreen mode Exit fullscreen mode

The class handles:

  • UID pseudonymization
  • HIPAA tag blanking
  • Private tag removal
  • Date reduction
  • File meta sanitization

What's Next

I'm planning:

  • GitHub Actions CI — test matrix across Python 3.10-3.13
  • PyPI publication — pip install dicom-clean
  • Docker image — for hospital IT deployments
  • Pixel OCR redaction — for burned-in PHI in images (commercial edition)

Feedback Welcome

I'd love feedback from anyone working with DICOM data, healthcare IT, or medical imaging.

What would you like to see next?


Tags: python healthcare opensource dicom privacy

Top comments (0)