Generic selectors
Exact matches only
Search in title
Search in content
Post Type Selectors

DICOM anonymization using Python

Dicom anonymization using Python tutorial – guide on hiding patient identity with Pydicom library, step-by-step code for anonymizing DICOM files, medical data privacy, and data protection in healthcare.

DICOM anonymization in Python is a tag-strip: open the file with pydicom (dcmread), overwrite the identifiers, save_as. This page is that how-to. It is not “pick an anonymizer product” and it is not “what is data anonymization.”

If you meant pick a tool (Pixelmed, CTP, DicomCleaner) → DICOM anonymizer software. If you meant what anonymization is (tabular / Safe Harbor / GDPR) → what is data anonymization. If you meant read a DICOM in Python, not strip it → how to read DICOM files. If you meant convert DICOM↔JPEG → how to convert a DICOM image into JPG or PNG. If you meant a PYCAD build → DICOM de-identification software (this URL stays here; do not treat that services page as this script).

What this script actually does

Layer Job This page
Direct identifiers Name, Patient ID, birth date, MRN-like IDs Overwrite or delete
Dates and sites Study / series / acquisition dates, institution, referring physician Blank or shift; do not leave a unique day
UIDs Study / Series / SOP Instance UID Replace with new UIDs so the file still parses
Burned-in pixels Name drawn on the image itself Not covered — that is OCR / pixel scrub, not a tag-strip

DICOM PS3.15 defines the Basic Confidentiality Profile: a checklist of attributes that must be removed or replaced before a dataset leaves a controlled environment. HIPAA Safe Harbor’s 18 identifiers (names, geo smaller than a state, dates except year, phones, SSN, MRN, device serials, full-face photos, …) land on many of the same tags. The general list lives on what is data anonymization. Here they become pydicom keywords.

Tag docs: pydicom user guide.

Environment

python -m venv dicom_anon
source dicom_anon/bin/activate   # Windows: dicom_anonScriptsactivate
pip install pydicom

glob2 is optional. The standard library glob / pathlib is enough for a folder of slices. Sanity check: import pydicom with no ImportError.

One file: dcmread, overwrite, save_as

import pydicom as pm

dicom_path = "path/to/dicom/file.dcm"
ds = pm.dcmread(dicom_path)
print("The patient name is:", ds.PatientName)

ds.PatientName = "Anonymous"
ds.save_as("anonymous_slice.dcm")

dcmread returns a FileDataset. Keywords (PatientName, PatientID) are easier than hex tags; both work. Setting PatientName = "Anonymous" and calling save_as is the whole mechanism. A missing keyword raises AttributeError — modalities omit tags. Use getattr(ds, "PatientName", "") or test with in ds before you write.

That one-tag script is not a Confidentiality Profile. Name-only still leaves Patient ID, birth date, Study Date, Accession Number, and the UIDs that stitch the series together.

A real tag-strip

import pydicom as pm
from pydicom.uid import generate_uid

# Keywords that map onto PS3.15 Basic Confidentiality / Safe Harbor.
# Delete if present; do not invent empty values for tags the IOD forbids.
STRIP = [
    "PatientName",
    "PatientID",
    "PatientBirthDate",
    "PatientBirthTime",
    "PatientSex",
    "PatientAge",
    "PatientAddress",
    "PatientTelephoneNumbers",
    "OtherPatientNames",
    "OtherPatientIDs",
    "EthnicGroup",
    "PatientComments",
    "ReferringPhysicianName",
    "PerformingPhysicianName",
    "OperatorsName",
    "InstitutionName",
    "InstitutionAddress",
    "InstitutionalDepartmentName",
    "StationName",
    "DeviceSerialNumber",
    "AccessionNumber",
    "StudyID",
    "RequestingPhysician",
]

DATE_TAGS = [
    "StudyDate",
    "SeriesDate",
    "AcquisitionDate",
    "ContentDate",
    "InstanceCreationDate",
]

def strip_dataset(ds, patient_name="Anonymous"):
    for keyword in STRIP:
        if keyword in ds:
            del ds[keyword]
    ds.PatientName = patient_name
    ds.PatientID = "ANON"
    for keyword in DATE_TAGS:
        if keyword in ds:
            ds.data_element(keyword).value = ""
    # New UIDs so the object still hangs together, but not the original ones.
    ds.StudyInstanceUID = generate_uid()
    ds.SeriesInstanceUID = generate_uid()
    ds.SOPInstanceUID = generate_uid()
    if hasattr(ds, "file_meta") and ds.file_meta is not None:
        ds.file_meta.MediaStorageSOPInstanceUID = ds.SOPInstanceUID
    return ds

ds = pm.dcmread("path/to/dicom/file.dcm")
strip_dataset(ds)
ds.save_as("anonymous_slice.dcm")

Dates: emptying them is the blunt option. A trial often shifts every date by the same offset so intervals stay useful and the calendar day does not. That is a policy choice, not a second library. Do not leave a raw birth date or a unique Study Date if the rest of the header is stripped — zip + date of birth + sex is the classic linkage attack on what is data anonymization.

UIDs: replacing Study / Series / SOP Instance UID is part of the profile. Keep the new Study UID the same across every slice of one study, or a viewer will treat the folder as unrelated files. The batch below does that.

A folder of slices

import os
from glob import glob
from pathlib import Path

import pydicom as pm
from pydicom.uid import generate_uid

def anonymize_dicom(in_path, out_path, patient_name="Anonymous",
                    study_uid=None, series_uid=None):
    ds = pm.dcmread(in_path)
    strip_dataset(ds, patient_name=patient_name)
    if study_uid:
        ds.StudyInstanceUID = study_uid
    if series_uid:
        ds.SeriesInstanceUID = series_uid
    if hasattr(ds, "file_meta") and ds.file_meta is not None:
        ds.file_meta.MediaStorageSOPInstanceUID = ds.SOPInstanceUID
    Path(out_path).parent.mkdir(parents=True, exist_ok=True)
    ds.save_as(out_path)

if __name__ == "__main__":
    src = "path/to/all/the/dicoms"
    dst = "path/to/anonymized"
    study_uid = generate_uid()
    series_uid = generate_uid()
    for src_path in glob(os.path.join(src, "*")):
        if not os.path.isfile(src_path):
            continue
        out_path = os.path.join(dst, os.path.basename(src_path))
        anonymize_dicom(src_path, out_path,
                        study_uid=study_uid, series_uid=series_uid)

Pass the same in_path and out_path if you intend to overwrite. Prefer a second directory until you have checked a slice in a viewer. PatientName defaults to Anonymous; pass another string if a trial protocol wants a study code instead.

Burned-in pixels

Some secondary captures and ultrasound overlays paint the name on the pixels. A header strip will not touch that. BurnedInAnnotation ((0028,0301)) is a flag, not a scrubber. Pixel OCR / black-box redaction is a different job and is not this script. If the flag is YES, do not call the file anonymized because the tags are clean.

What this page is not

This is not a product picker — that is DICOM anonymizer software. It is not a Safe Harbor essay — that is what is data anonymization. It is not dcmread + .pixel_array — that is how to read DICOM files. k-anonymity (every record indistinguishable from k−1 others) is a tabular guarantee, not a DICOM tag list; it is named on the privacy page, not implemented here.

PYCAD builds imaging pipelines that have to strip these tags for real. Case studies.

We build custom medical imaging platforms — advanced DICOM viewers, AI segmentation, and the clinical systems around them.

Get in Touch

Copyright © 2026 PYCAD. All Rights Reserved.