---
title: DICOM anonymization
date: 2026-07-19T15:39:38Z
modified: 2026-07-19T15:39:41Z
permalink: "https://www.micheledpierri.com/2026/07/19/dicom-anonymization-practical-guide/"
type: post
status: publish
excerpt: ""
wpid: 2984
categories:
  - Health Informatics
  - Medicine
  - Programming
tags:
  - Health Informatics
  - Medicine
  - Programming
  - Dicom
  - Medical Imaging
  - Python
featured_image: "https://www.micheledpierri.com/wp-content/uploads/2026/07/dicom_anonymization_.png"
featured_image_alt: A puzzled early 20th-century doctor examines a medical chart with the patient’s name erased while the patient lies in a modest hospital ward.
timestamp: 2026-07-19T15:39:41Z
---

---

Deleting the patient name from a DICOM file feels like anonymization. It is not.

A cardiac CT study I once pulled for a teaching file had a blank `PatientName`, a scrubbed `PatientID`, and a perfectly readable date of birth sitting three tags down in a private block the vendor never documented. The header looked clean. It was not. And this is the recurring trap: DICOM was designed to carry identity, not to shed it. Anyone who has tried to share imaging for a multicentre study, a public dataset, a conference talk, or an AI training pipeline runs into the same wall. The name is the easy part.

This piece is for people who write the pipeline, not just click “anonymize” in a viewer. What follows: why de-identification is a legal and technical obligation, exactly where protected health information (PHI) hides inside a DICOM object, the DICOM standard’s own framework for cleaning it, the pixel-level identity problem that headers cannot touch, the truth about “anonymous” formats like NIfTI, and Python you can actually run.

---

## Why bother at all?

Three forces converge here, and they rarely align neatly.

The first is regulatory. Under the GDPR, truly anonymous data falls outside the Regulation entirely (Recital 26), while _pseudonymized_ data remains personal data with all the obligations that implies. That distinction is not pedantry. It decides whether you need a legal basis, a data protection impact assessment, and a data processing agreement, or whether you are free to publish. In the United States, HIPAA offers two roads: Safe Harbor, which enumerates eighteen identifier categories to strip, and Expert Determination, a statistical risk argument certified by a qualified person. Europe has no equivalent bright-line list, which is why so many EU imaging pipelines borrow the Safe Harbor eighteen as a working floor.

The second force is scientific reproducibility. Journals, funders, and registries increasingly demand shareable data. You cannot share what you cannot de-identify defensibly.

The third is the AI pipeline. Training a model on cardiac imaging means moving thousands of studies across trust boundaries, often to cloud compute. Every hop is an exposure.

So the question is not _whether_ to de-identify. It is whether your de-identification survives an adversary who actually tries. Most do not.

---

## Anonymization, pseudonymization, de-identification: not synonyms

These words get used interchangeably, and the sloppiness has consequences.

**Anonymization** aims to make re-identification impossible, or at least not reasonably likely, with no key retained anywhere. **Pseudonymization** replaces identifiers with a code while a re-linking key is held separately, under access control. Most clinical research uses the latter and calls it the former. That mislabelling is where audits go badly.

**De-identification** is the umbrella process, and DICOM uses this term deliberately. The standard is blunt about it: applying the confidentiality profiles “does not guarantee that all individually identifying information will be removed.” De-identifying the attributes does not de-identify the _information object_. Read that twice. The standard authors knew that a conformant header scrub is necessary but not sufficient.

For a coronary registry submission versus a public dataset versus an internal model, the acceptable residual risk differs. One profile does not fit all. David Clunie, who edited the DICOM supplement that became PS3.15 Annex E, made exactly this critique of blanket tool comparisons: overzealous stripping produces data that is safe and useless, while lax stripping produces data that is useful and dangerous. Judging a tool by its defaults, he argued, misses how easily those defaults can be reconfigured for the use case at hand. The engineering lives in that tension.

---

## Where PHI actually hides

If you only scrub the obvious tags, you will leak. Here is the real surface area.

**Standard header attributes.** `PatientName (0010,0010)`, `PatientID (0010,0020)`, `PatientBirthDate (0010,0030)`, `PatientAddress`, `ReferringPhysicianName`, `InstitutionName`, `AccessionNumber`, `StationName`, and dozens more. These are the ones every tool handles.

**Dates and times.** Acquisition, study, series, and content dates let an adversary reconstruct a timeline and cross-reference it with, say, a press release about a public figure’s surgery. The DICOM value representations DA, DT, and TM are worth handling _by VR_, not by a hand-maintained tag list, because new date tags appear as the standard evolves.

**UIDs.** `StudyInstanceUID`, `SeriesInstanceUID`, `SOPInstanceUID`, and `FrameOfReferenceUID` are globally unique. If any un-remapped UID also lives in the source PACS, you have a join key straight back to the record. Remap them, but remap them _consistently_, or you shatter the study/series/instance hierarchy.

**Private tags.** Vendor-specific, odd-group, frequently undocumented. This is where my “clean” cardiac CT hid its date of birth. The safe default is to remove all private elements unless a specific one is known safe and needed.

**Structured content and overlays.** Structured Reports, curve data, graphic annotations, and overlay planes can carry names and free text. Radiotherapy and echo objects are especially prone to this.

**Burned-in pixel PHI.** Ultrasound frames, secondary captures, and screen grabs routinely stamp the patient’s name and MRN directly into the pixels. The `BurnedInAnnotation (0028,0301)` flag is supposed to warn you. Would you trust a flag a technologist may never have set? I would not.

**File meta and the 128-byte preamble.** The standard requires replacing the File Meta Information, including the preamble, because application entity titles and implementation details leak there too.

Miss any one of these categories and the header “looks” anonymous while remaining trivially reversible.

---

## The standard has already thought about this: PS3.15 Annex E

DICOM does not leave you to invent a scheme. Part 15, Annex E defines the **Basic Application Level Confidentiality Profile** plus a set of _options_ you compose on top of it. Table E.1-1 lists every attribute and the action to apply.

The action codes are the vocabulary worth memorizing:

- **D**: replace with a non-zero dummy value
- **Z**: replace with zero-length or dummy
- **X**: remove the attribute entirely
- **K**: keep unchanged
- **C**: clean (retain but scrub embedded identifiers)
- **U**: replace UID with a consistently remapped one

The options let you tune the profile to the use case rather than nuking everything:

- **Clean Pixel Data**: deal with burned-in identifiers
- **Clean Recognizable Visual Features**: the defacing hook
- **Retain Longitudinal Temporal Information (with Modified Dates)**: preserve intervals for time-series work while shifting absolute dates
- **Retain UIDs**: when downstream linkage is legitimately needed
- **Retain Safe Private**: keep vendor acquisition parameters flagged safe
- **Retain Patient Characteristics**: keep age, sex, weight for analysis

Conform to the Basic Profile, then declare which options you applied, and set `PatientIdentityRemoved (0012,0062)` to `YES` with a machine-readable `DeidentificationMethodCodeSequence`. That declaration is what makes your output auditable. Why reinvent a tag list when NEMA maintains a normative one that tracks the standard?

---

## The part headers cannot fix: pixel-level identity

Here is the finding that should unsettle anyone building head-and-neck or cardiac-thoracic pipelines.

Schwarz and colleagues, writing in the _New England Journal of Medicine_ in 2019, took 84 volunteers, reconstructed 3D facial surfaces from their otherwise de-identified brain MRIs, and ran commercial face-recognition software against ordinary photographs. The correct scan was the top match for 70 of 84 people. That is 83%. The correct scan sat in the top five for 80 of 84, or 95%. A perfectly de-identified header protected none of them, because their faces were in the voxels.

This is not limited to the brain. Cardiac and thoracic CT frequently includes the mandible, orbits, and facial soft tissue at the top of the volume. Chest imaging carries a subtler problem: Packhäuser and colleagues showed in 2022 that deep learning can re-identify patients from chest X-rays using the _biometric_ signature of the anatomy itself, no face required.

The mitigations are volumetric, not header-based. Defacing removes or blurs facial surface voxels. Skull-stripping (brain extraction) discards everything outside the brain. Both degrade the image for anything that needs facial or sinonasal anatomy, which is the recurring complaint from the ENT and maxillofacial side. For a coronary CTA you rarely need the face; for a study of aortic root geometry that extends cranially, you might have to think harder. There is no free lunch here, and pretending otherwise is how public datasets end up re-identifiable.

---

## Are “anonymous” formats like NIfTI a solution?

Short answer: no, and believing so is dangerous.

NIfTI (and Analyze before it, and MINC alongside) was built for neuroimaging analysis, not for privacy. Its appeal for de-identification is incidental: the format simply _cannot represent_ most DICOM header fields, so converting DICOM to NIfTI drops `PatientName`, `PatientID`, dates, and the whole private-tag zoo by omission. It looks anonymous because it is impoverished.

Three problems follow.

First, conversion does nothing to the pixels. The face is still in the volume. A NIfTI of a head MRI is exactly as re-identifiable as the DICOM it came from, per Schwarz.

Second, the metadata does not vanish, it _relocates_. The dominant conversion tool, `dcm2niix`, emits a JSON sidecar (the BIDS convention) that can carry acquisition dates, device serial numbers, and institution strings. De-identify the NIfTI and forget the sidecar, and you have leaked through the side door. I have seen exactly this in a shared dataset.

Third, filenames. Pipelines love to name files `Rossi_Mario_20240312.nii.gz`. The format is anonymous; your naming convention is not.

NIfTI is a fine _analysis_ format and a poor _anonymization strategy_. Treat conversion as one step, never the whole story.

---

## Techniques and working code

Enough theory. Here is the operational core, in Python, using `pydicom`.

### 1. Before anything else: look at what is actually in the file

Every de-identification failure I have seen started the same way, with someone scrubbing a tag list they assumed was complete. You cannot clean what you have never inspected. So the first script in any pipeline is not a scrubber, it is an auditor.

The crude version is one line:

import pydicom

ds = pydicom.dcmread("input.dcm")
print(ds)   # full dataset dump, human readable
```
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> pydicom</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">ds </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> pydicom.dcmread(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">input.dcm</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(ds)   </span><span style="color: #6272A4"># full dataset dump, human readable</span></span>
<span class="line"></span>
```

That prints everything, which for a multi-frame cardiac study means thousands of lines you will not read. Useful once. Useless as a habit.

What you actually want is a structured walk that recurses into sequences, flags private blocks, and separates the elements by risk category:

import pydicom
from pydicom.dataset import Dataset

def inspect(ds: Dataset, show_pixel_data: bool = False) -> None:
    """Recursive audit of a DICOM dataset, sequences included."""

    def _walk(dataset, depth=0):
        pad = "  " * depth
        for elem in dataset:
            # PixelData is megabytes of noise in a console
            if elem.tag == 0x7FE00010 and not show_pixel_data:
                print(f"{pad}{elem.tag} PixelData -> "
                      f"[{len(elem.value)} bytes, suppressed]")
                continue

            private = "PRIV" if elem.tag.is_private else "    "
            print(f"{pad}{private} {elem.tag} {elem.VR} "
                  f"{elem.name:<40} = {str(elem.value)[:60]}")

            # Sequences nest. PHI hides in the nesting.
            if elem.VR == "SQ":
                for i, item in enumerate(elem.value):
                    print(f"{pad}  -- item {i} --")
                    _walk(item, depth + 2)

    print("=== FILE META ===")
    _walk(ds.file_meta)
    print("\n=== DATASET ===")
    _walk(ds)

ds = pydicom.dcmread("input.dcm")
inspect(ds)
```
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> pydicom</span></span>
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> pydicom.dataset </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> Dataset</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">inspect</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">ds</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> Dataset, </span><span style="color: #FFB86C; font-style: italic">show_pixel_data</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">bool</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">False</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">None</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4">"""Recursive audit of a DICOM dataset, sequences included."""</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">_walk</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">dataset</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">depth</span><span style="color: #FF79C6">=</span><span style="color: #BD93F9">0</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">        pad </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">  </span><span style="color: #E9F284">"</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">*</span><span style="color: #F8F8F2"> depth</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> elem </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> dataset:</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #6272A4"># PixelData is megabytes of noise in a console</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.tag </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">0x</span><span style="color: #BD93F9">7FE00010</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">not</span><span style="color: #F8F8F2"> show_pixel_data:</span></span>
<span class="line"><span style="color: #F8F8F2">                </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">pad</span><span style="color: #BD93F9">}{</span><span style="color: #F8F8F2">elem.tag</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> PixelData -> "</span></span>
<span class="line"><span style="color: #F8F8F2">                      </span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"[</span><span style="color: #BD93F9">{</span><span style="color: #8BE9FD">len</span><span style="color: #F8F8F2">(elem.value)</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> bytes, suppressed]"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">                </span><span style="color: #FF79C6">continue</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">            private </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PRIV</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.tag.is_private </span><span style="color: #FF79C6">else</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">    </span><span style="color: #E9F284">"</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">pad</span><span style="color: #BD93F9">}{</span><span style="color: #F8F8F2">private</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">elem.tag</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">elem.</span><span style="color: #BD93F9">VR}</span><span style="color: #F1FA8C"> "</span></span>
<span class="line"><span style="color: #F8F8F2">                  </span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">elem.name</span><span style="color: #FF79C6">:<40</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> = </span><span style="color: #BD93F9">{</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)[:</span><span style="color: #BD93F9">60</span><span style="color: #F8F8F2">]</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #6272A4"># Sequences nest. PHI hides in the nesting.</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.</span><span style="color: #BD93F9">VR</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SQ</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">                </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> i, item </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD">enumerate</span><span style="color: #F8F8F2">(elem.value):</span></span>
<span class="line"><span style="color: #F8F8F2">                    </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">pad</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">  -- item </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">i</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> --"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">                    _walk(item, depth </span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">2</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">=== FILE META ===</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">    _walk(ds.file_meta)</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #E9F284">"</span><span style="color: #FF79C6">\n</span><span style="color: #F1FA8C">=== DATASET ===</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">    _walk(ds)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">ds </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> pydicom.dcmread(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">input.dcm</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">inspect(ds)</span></span>
<span class="line"></span>
```

The recursion matters. PS3.15 requires acting on the listed attributes “whether contained in the main dataset or embedded in an Item of a Sequence of Items”, and a flat loop over `ds` silently skips every nested item. Referenced study sequences, request attributes, and source image sequences are classic hiding places.

Now the part that earns its keep: a risk triage that tells you where to look, rather than dumping everything.

from collections import defaultdict

# Names that should never survive de-identification
DIRECT_IDENTIFIERS = {
    "PatientName", "PatientID", "PatientBirthDate", "PatientAddress",
    "PatientTelephoneNumbers", "OtherPatientIDs", "OtherPatientNames",
    "ReferringPhysicianName", "PerformingPhysicianName", "OperatorsName",
    "PhysiciansOfRecord", "NameOfPhysiciansReadingStudy",
    "InstitutionName", "InstitutionAddress", "StationName",
    "AccessionNumber", "StudyID", "IssuerOfPatientID",
}

# Free-text fields where operators type anything, names included
FREE_TEXT = {
    "StudyDescription", "SeriesDescription", "ImageComments",
    "PatientComments", "AdditionalPatientHistory", "RequestedProcedureDescription",
    "PerformedProcedureStepDescription", "DerivationDescription",
}

def audit(ds: Dataset) -> dict:
    findings = defaultdict(list)

    def _scan(dataset, path=""):
        for elem in dataset:
            loc = f"{path}{elem.name}"

            if elem.tag.is_private:
                findings["private"].append((str(elem.tag), loc, str(elem.value)[:50]))
            elif elem.name in DIRECT_IDENTIFIERS and elem.value not in ("", None):
                findings["direct"].append((str(elem.tag), loc, str(elem.value)[:50]))
            elif elem.name in FREE_TEXT and elem.value:
                findings["free_text"].append((str(elem.tag), loc, str(elem.value)[:50]))
            elif elem.VR in ("DA", "DT", "TM") and elem.value:
                findings["temporal"].append((str(elem.tag), loc, str(elem.value)))
            elif elem.VR == "UI" and elem.name.endswith("UID"):
                findings["uid"].append((str(elem.tag), loc, str(elem.value)))

            if elem.VR == "SQ":
                for i, item in enumerate(elem.value):
                    _scan(item, path=f"{loc}[{i}]/")

    _scan(ds)

    # Pixel-level risk flags
    if ds.get("BurnedInAnnotation", "").upper() == "YES":
        findings["pixel"].append(("(0028,0301)", "BurnedInAnnotation", "YES"))
    if ds.get("Modality") in ("US", "SC", "XC", "OT"):
        findings["pixel"].append(("(0008,0060)", "Modality",
                                  f"{ds.Modality}: burned-in text likely"))
    if "OverlayData" in ds or (0x6000, 0x3000) in ds:
        findings["pixel"].append(("(6000,3000)", "OverlayData", "overlay plane present"))

    return dict(findings)

report = audit(ds)
for category, items in report.items():
    print(f"\n### {category.upper()}  ({len(items)} findings)")
    for tag, name, value in items:
        print(f"  {tag}  {name} = {value}")
```
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> collections </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> defaultdict</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># Names that should never survive de-identification</span></span>
<span class="line"><span style="color: #BD93F9">DIRECT_IDENTIFIERS</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> {</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientBirthDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientAddress</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientTelephoneNumbers</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OtherPatientIDs</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OtherPatientNames</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">ReferringPhysicianName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PerformingPhysicianName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OperatorsName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PhysiciansOfRecord</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">NameOfPhysiciansReadingStudy</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">InstitutionName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">InstitutionAddress</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StationName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">AccessionNumber</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StudyID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">IssuerOfPatientID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># Free-text fields where operators type anything, names included</span></span>
<span class="line"><span style="color: #BD93F9">FREE_TEXT</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> {</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StudyDescription</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SeriesDescription</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">ImageComments</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientComments</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">AdditionalPatientHistory</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">RequestedProcedureDescription</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PerformedProcedureStepDescription</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">DerivationDescription</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">}</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">audit</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">ds</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> Dataset) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">dict</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">    findings </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> defaultdict(</span><span style="color: #8BE9FD; font-style: italic">list</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">_scan</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">dataset</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">path</span><span style="color: #FF79C6">=</span><span style="color: #E9F284">""</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> elem </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> dataset:</span></span>
<span class="line"><span style="color: #F8F8F2">            loc </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">path</span><span style="color: #BD93F9">}{</span><span style="color: #F8F8F2">elem.name</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">"</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.tag.is_private:</span></span>
<span class="line"><span style="color: #F8F8F2">                findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">private</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.tag), loc, </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)[:</span><span style="color: #BD93F9">50</span><span style="color: #F8F8F2">]))</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">elif</span><span style="color: #F8F8F2"> elem.name </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">DIRECT_IDENTIFIERS</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> elem.value </span><span style="color: #FF79C6">not</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> (</span><span style="color: #E9F284">""</span><span style="color: #F8F8F2">, </span><span style="color: #BD93F9">None</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">                findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">direct</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.tag), loc, </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)[:</span><span style="color: #BD93F9">50</span><span style="color: #F8F8F2">]))</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">elif</span><span style="color: #F8F8F2"> elem.name </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">FREE_TEXT</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> elem.value:</span></span>
<span class="line"><span style="color: #F8F8F2">                findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">free_text</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.tag), loc, </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)[:</span><span style="color: #BD93F9">50</span><span style="color: #F8F8F2">]))</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">elif</span><span style="color: #F8F8F2"> elem.</span><span style="color: #BD93F9">VR</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> (</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">DA</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">DT</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">TM</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> elem.value:</span></span>
<span class="line"><span style="color: #F8F8F2">                findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">temporal</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.tag), loc, </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)))</span></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">elif</span><span style="color: #F8F8F2"> elem.</span><span style="color: #BD93F9">VR</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">UI</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> elem.name.endswith(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">UID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">                findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">uid</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.tag), loc, </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">(elem.value)))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">            </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.</span><span style="color: #BD93F9">VR</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SQ</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">                </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> i, item </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD">enumerate</span><span style="color: #F8F8F2">(elem.value):</span></span>
<span class="line"><span style="color: #F8F8F2">                    _scan(item, </span><span style="color: #FFB86C; font-style: italic">path</span><span style="color: #FF79C6">=</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">loc</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">[</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">i</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">]/"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    _scan(ds)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># Pixel-level risk flags</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> ds.get(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">BurnedInAnnotation</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">""</span><span style="color: #F8F8F2">).upper() </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">YES</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">        findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">pixel</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">(0028,0301)</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">BurnedInAnnotation</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">YES</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">))</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> ds.get(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">Modality</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> (</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">US</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SC</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">XC</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OT</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">        findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">pixel</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">(0008,0060)</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">Modality</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">                                  </span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">ds.Modality</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">: burned-in text likely"</span><span style="color: #F8F8F2">))</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OverlayData</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> ds </span><span style="color: #FF79C6">or</span><span style="color: #F8F8F2"> (</span><span style="color: #FF79C6">0x</span><span style="color: #BD93F9">6000</span><span style="color: #F8F8F2">, </span><span style="color: #FF79C6">0x</span><span style="color: #BD93F9">3000</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> ds:</span></span>
<span class="line"><span style="color: #F8F8F2">        findings[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">pixel</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">].append((</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">(6000,3000)</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OverlayData</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">overlay plane present</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">))</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">dict</span><span style="color: #F8F8F2">(findings)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">report </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> audit(ds)</span></span>
<span class="line"><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> category, items </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> report.items():</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"</span><span style="color: #FF79C6">\n</span><span style="color: #F1FA8C">### </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">category.upper()</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">  (</span><span style="color: #BD93F9">{</span><span style="color: #8BE9FD">len</span><span style="color: #F8F8F2">(items)</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> findings)"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> tag, name, value </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> items:</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(</span><span style="color: #FF79C6">f</span><span style="color: #F1FA8C">"  </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">tag</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">  </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">name</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C"> = </span><span style="color: #BD93F9">{</span><span style="color: #F8F8F2">value</span><span style="color: #BD93F9">}</span><span style="color: #F1FA8C">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
```

Run this on a handful of studies from each scanner in your institution before you write a single line of scrubbing code. The output is frequently sobering. On our CT scanners the private block alone routinely holds forty or more undocumented elements, and reading them is how I found that date of birth.

Two habits worth building. First, run the audit again _after_ de-identification on the output files: the `direct`, `private`, and `temporal` buckets should be empty or deliberately justified. That closes the loop, and it is the same code. Second, if you want the standard’s own view rather than a hand-rolled list, `pydicom` exposes the confidentiality profiles directly:

from pydicom._dicom_dict import DicomDictionary

# Which tags does PS3.15 Basic Profile actually touch?
# The Stanford `deid` package ships machine-readable recipes for this.
from deid.config import DeidRecipe
recipe = DeidRecipe()          # loads the default PS3.15-derived recipe
print(recipe.get_actions()[:10])
```
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> pydicom._dicom_dict </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> DicomDictionary</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># Which tags does PS3.15 Basic Profile actually touch?</span></span>
<span class="line"><span style="color: #6272A4"># The Stanford `deid` package ships machine-readable recipes for this.</span></span>
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> deid.config </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> DeidRecipe</span></span>
<span class="line"><span style="color: #F8F8F2">recipe </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> DeidRecipe()          </span><span style="color: #6272A4"># loads the default PS3.15-derived recipe</span></span>
<span class="line"><span style="color: #8BE9FD">print</span><span style="color: #F8F8F2">(recipe.get_actions()[:</span><span style="color: #BD93F9">10</span><span style="color: #F8F8F2">])</span></span>
<span class="line"></span>
```

For a one-off visual check outside Python, `dcmdump` (from DCMTK) and the `gdcmdump --print` command both give a fast, complete textual dump, and DCMTK’s `dcmdump +P` lets you query single tags in a shell loop. Handy for scripting a quick institutional survey.

### 2. Header scrubbing with a proper de-identification declaration

import pydicom
from pydicom.dataset import Dataset

# Direct identifiers to empty (Z-style) or remove (X-style)
BLANK_TAGS = [
    "PatientName", "PatientID", "PatientBirthDate", "PatientSex",
    "OtherPatientIDs", "OtherPatientNames", "PatientAddress",
    "PatientTelephoneNumbers", "PatientMotherBirthName",
    "ReferringPhysicianName", "PerformingPhysicianName",
    "PhysiciansOfRecord", "OperatorsName", "NameOfPhysiciansReadingStudy",
    "InstitutionName", "InstitutionAddress", "InstitutionalDepartmentName",
    "StationName", "AccessionNumber", "StudyID",
]

def deidentify_header(ds: Dataset) -> Dataset:
    # 1. Remove ALL private tags. The single most common leak source.
    ds.remove_private_tags()

    # 2. Blank direct identifiers that exist in this object
    for tag in BLANK_TAGS:
        if tag in ds:
            ds.data_element(tag).value = ""

    # 3. Strip anything with a date/time VR by VR, not by name,
    #    so new date tags in future IODs are still caught.
    def _scrub_dates(dataset, elem):
        if elem.VR in ("DA", "DT", "TM"):
            elem.value = ""
    ds.walk(_scrub_dates)

    # 4. Declare what we did (PS3.15 conformance signal)
    ds.PatientIdentityRemoved = "YES"
    ds.DeidentificationMethod = "Custom pydicom pipeline, PS3.15 Basic Profile"

    return ds

ds = pydicom.dcmread("input.dcm")
ds = deidentify_header(ds)
ds.save_as("output_deid.dcm")
```
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> pydicom</span></span>
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> pydicom.dataset </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> Dataset</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># Direct identifiers to empty (Z-style) or remove (X-style)</span></span>
<span class="line"><span style="color: #BD93F9">BLANK_TAGS</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> [</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientBirthDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientSex</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OtherPatientIDs</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OtherPatientNames</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientAddress</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientTelephoneNumbers</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PatientMotherBirthName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">ReferringPhysicianName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PerformingPhysicianName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">PhysiciansOfRecord</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">OperatorsName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">NameOfPhysiciansReadingStudy</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">InstitutionName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">InstitutionAddress</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">InstitutionalDepartmentName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StationName</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">AccessionNumber</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StudyID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">]</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">deidentify_header</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">ds</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> Dataset) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> Dataset:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># 1. Remove ALL private tags. The single most common leak source.</span></span>
<span class="line"><span style="color: #F8F8F2">    ds.remove_private_tags()</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># 2. Blank direct identifiers that exist in this object</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">BLANK_TAGS</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> ds:</span></span>
<span class="line"><span style="color: #F8F8F2">            ds.data_element(tag).value </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">""</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># 3. Strip anything with a date/time VR by VR, not by name,</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4">#    so new date tags in future IODs are still caught.</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">_scrub_dates</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">dataset</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">elem</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> elem.</span><span style="color: #BD93F9">VR</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> (</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">DA</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">DT</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">TM</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">):</span></span>
<span class="line"><span style="color: #F8F8F2">            elem.value </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">""</span></span>
<span class="line"><span style="color: #F8F8F2">    ds.walk(_scrub_dates)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># 4. Declare what we did (PS3.15 conformance signal)</span></span>
<span class="line"><span style="color: #F8F8F2">    ds.PatientIdentityRemoved </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">YES</span><span style="color: #E9F284">"</span></span>
<span class="line"><span style="color: #F8F8F2">    ds.DeidentificationMethod </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">Custom pydicom pipeline, PS3.15 Basic Profile</span><span style="color: #E9F284">"</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> ds</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">ds </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> pydicom.dcmread(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">input.dcm</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">ds </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> deidentify_header(ds)</span></span>
<span class="line"><span style="color: #F8F8F2">ds.save_as(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">output_deid.dcm</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
```

Note what this does _not_ do yet: it flattens all dates, which breaks longitudinal analysis. In practice you almost never want that. Keep reading.

### 3. Consistent UID remapping (preserve the study hierarchy)

If you randomize UIDs independently, series stop belonging to studies and instances stop belonging to series. Use deterministic generation so the same source UID always maps to the same new UID across every file.

from pydicom.uid import generate_uid

# Your organization's registered UID root. Do not use a made-up one
# in production. Register through your national body or IANA.
ORG_ROOT = "1.2.826.0.1.3680043.10.9999"
PROJECT_SALT = "cardiac-registry-2026"  # keep secret if you want irreversibility

def remap_uid(original_uid: str) -> str:
    # entropy_srcs makes the output deterministic AND collision-resistant:
    # same inputs -> same UID, every time, in every file.
    return generate_uid(prefix=ORG_ROOT + ".",
                        entropy_srcs=[PROJECT_SALT, original_uid])

for uid_tag in ["StudyInstanceUID", "SeriesInstanceUID",
                "SOPInstanceUID", "FrameOfReferenceUID"]:
    if uid_tag in ds:
        ds.data_element(uid_tag).value = remap_uid(ds.data_element(uid_tag).value)

# The SOPInstanceUID also lives in file meta; keep them in sync
ds.file_meta.MediaStorageSOPInstanceUID = ds.SOPInstanceUID
```
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> pydicom.uid </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> generate_uid</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># Your organization's registered UID root. Do not use a made-up one</span></span>
<span class="line"><span style="color: #6272A4"># in production. Register through your national body or IANA.</span></span>
<span class="line"><span style="color: #BD93F9">ORG_ROOT</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">1.2.826.0.1.3680043.10.9999</span><span style="color: #E9F284">"</span></span>
<span class="line"><span style="color: #BD93F9">PROJECT_SALT</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">cardiac-registry-2026</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">  </span><span style="color: #6272A4"># keep secret if you want irreversibility</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">remap_uid</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">original_uid</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># entropy_srcs makes the output deterministic AND collision-resistant:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># same inputs -> same UID, every time, in every file.</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> generate_uid(</span><span style="color: #FFB86C; font-style: italic">prefix</span><span style="color: #FF79C6">=</span><span style="color: #BD93F9">ORG_ROOT</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">.</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">                        </span><span style="color: #FFB86C; font-style: italic">entropy_srcs</span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2">[</span><span style="color: #BD93F9">PROJECT_SALT</span><span style="color: #F8F8F2">, original_uid])</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> uid_tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> [</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StudyInstanceUID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SeriesInstanceUID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">,</span></span>
<span class="line"><span style="color: #F8F8F2">                </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SOPInstanceUID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">FrameOfReferenceUID</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">]:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> uid_tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> ds:</span></span>
<span class="line"><span style="color: #F8F8F2">        ds.data_element(uid_tag).value </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> remap_uid(ds.data_element(uid_tag).value)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6272A4"># The SOPInstanceUID also lives in file meta; keep them in sync</span></span>
<span class="line"><span style="color: #F8F8F2">ds.file_meta.MediaStorageSOPInstanceUID </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> ds.SOPInstanceUID</span></span>
<span class="line"></span>
```

Keep the salt secret and unrecoverable and this is anonymization. Store it in a key vault and it is pseudonymization. The code is identical; the governance is not.

### 4. Date shifting instead of date deletion

The “Retain Longitudinal Temporal Information with Modified Dates” option, done right. Every date for a given patient shifts by the _same_ random offset, so intervals between visits are preserved while absolute dates become meaningless.

import hashlib
from datetime import datetime, timedelta

def patient_offset(patient_uid: str, secret: str, max_days: int = 730) -> int:
    # Deterministic per-patient shift in [-max_days, 0]
    h = int(hashlib.sha256((secret + patient_uid).encode()).hexdigest(), 16)
    return -(h % max_days)

def shift_da(da: str, offset_days: int) -> str:
    if not da:
        return da
    d = datetime.strptime(da, "%Y%m%d")
    return (d + timedelta(days=offset_days)).strftime("%Y%m%d")

offset = patient_offset(original_patient_id, PROJECT_SALT)
for tag in ["StudyDate", "SeriesDate", "AcquisitionDate", "ContentDate"]:
    if tag in ds and ds.data_element(tag).value:
        ds.data_element(tag).value = shift_da(ds.data_element(tag).value, offset)
```
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> hashlib</span></span>
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> datetime </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> datetime, timedelta</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">patient_offset</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">patient_uid</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">secret</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">max_days</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">int</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">730</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">int</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># Deterministic per-patient shift in [-max_days, 0]</span></span>
<span class="line"><span style="color: #F8F8F2">    h </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">int</span><span style="color: #F8F8F2">(hashlib.sha256((secret </span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2"> patient_uid).encode()).hexdigest(), </span><span style="color: #BD93F9">16</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2">(h </span><span style="color: #FF79C6">%</span><span style="color: #F8F8F2"> max_days)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">shift_da</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">da</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">, </span><span style="color: #FFB86C; font-style: italic">offset_days</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">int</span><span style="color: #F8F8F2">) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">str</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">not</span><span style="color: #F8F8F2"> da:</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> da</span></span>
<span class="line"><span style="color: #F8F8F2">    d </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> datetime.strptime(da, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">%Y%m</span><span style="color: #BD93F9">%d</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> (d </span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2"> timedelta(</span><span style="color: #FFB86C; font-style: italic">days</span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2">offset_days)).strftime(</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">%Y%m</span><span style="color: #BD93F9">%d</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">offset </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> patient_offset(original_patient_id, </span><span style="color: #BD93F9">PROJECT_SALT</span><span style="color: #F8F8F2">)</span></span>
<span class="line"><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> [</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">StudyDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">SeriesDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">AcquisitionDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">, </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">ContentDate</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">]:</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> tag </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> ds </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> ds.data_element(tag).value:</span></span>
<span class="line"><span style="color: #F8F8F2">        ds.data_element(tag).value </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> shift_da(ds.data_element(tag).value, offset)</span></span>
<span class="line"></span>
```

For a serial imaging study of ventricular remodelling, this is the difference between usable and destroyed data.

### 5. Burned-in pixel PHI: detect and redact

Do not trust `BurnedInAnnotation`. For modalities that stamp text (US, SC, screen captures), OCR the frame and black out any text region.

import numpy as np
import pytesseract
from PIL import Image

def redact_burned_in_text(ds: Dataset) -> Dataset:
    arr = ds.pixel_array
    # Normalize to 8-bit grayscale for the OCR engine
    lo, hi = float(arr.min()), float(arr.max())
    img8 = np.zeros_like(arr, dtype=np.uint8) if hi == lo else \
           (255 * (arr.astype(np.float32) - lo) / (hi - lo)).astype(np.uint8)

    data = pytesseract.image_to_data(
        Image.fromarray(img8), output_type=pytesseract.Output.DICT
    )
    redacted = False
    for i, txt in enumerate(data["text"]):
        if txt.strip() and int(data["conf"][i]) > 40:
            x, y, w, h = (data["left"][i], data["top"][i],
                          data["width"][i], data["height"][i])
            arr[y:y+h, x:x+w] = arr.min()   # blackout
            redacted = True

    if redacted:
        ds.PixelData = arr.tobytes()
        ds.BurnedInAnnotation = "NO"
    return ds
```
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> numpy </span><span style="color: #FF79C6">as</span><span style="color: #F8F8F2"> np</span></span>
<span class="line"><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> pytesseract</span></span>
<span class="line"><span style="color: #FF79C6">from</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">PIL</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">import</span><span style="color: #F8F8F2"> Image</span></span>
<span class="line"></span>
<span class="line"><span style="color: #FF79C6">def</span><span style="color: #F8F8F2"> </span><span style="color: #50FA7B">redact_burned_in_text</span><span style="color: #F8F8F2">(</span><span style="color: #FFB86C; font-style: italic">ds</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2"> Dataset) </span><span style="color: #FF79C6">-></span><span style="color: #F8F8F2"> Dataset:</span></span>
<span class="line"><span style="color: #F8F8F2">    arr </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> ds.pixel_array</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #6272A4"># Normalize to 8-bit grayscale for the OCR engine</span></span>
<span class="line"><span style="color: #F8F8F2">    lo, hi </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">float</span><span style="color: #F8F8F2">(arr.min()), </span><span style="color: #8BE9FD; font-style: italic">float</span><span style="color: #F8F8F2">(arr.max())</span></span>
<span class="line"><span style="color: #F8F8F2">    img8 </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> np.zeros_like(arr, </span><span style="color: #FFB86C; font-style: italic">dtype</span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2">np.uint8) </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> hi </span><span style="color: #FF79C6">==</span><span style="color: #F8F8F2"> lo </span><span style="color: #FF79C6">else</span><span style="color: #F8F8F2"> \</span></span>
<span class="line"><span style="color: #F8F8F2">           (</span><span style="color: #BD93F9">255</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">*</span><span style="color: #F8F8F2"> (arr.astype(np.float32) </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2"> lo) </span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2"> (hi </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2"> lo)).astype(np.uint8)</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    data </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> pytesseract.image_to_data(</span></span>
<span class="line"><span style="color: #F8F8F2">        Image.fromarray(img8), </span><span style="color: #FFB86C; font-style: italic">output_type</span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2">pytesseract.Output.</span><span style="color: #BD93F9">DICT</span></span>
<span class="line"><span style="color: #F8F8F2">    )</span></span>
<span class="line"><span style="color: #F8F8F2">    redacted </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">False</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">for</span><span style="color: #F8F8F2"> i, txt </span><span style="color: #FF79C6">in</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD">enumerate</span><span style="color: #F8F8F2">(data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">text</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">]):</span></span>
<span class="line"><span style="color: #F8F8F2">        </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> txt.strip() </span><span style="color: #FF79C6">and</span><span style="color: #F8F8F2"> </span><span style="color: #8BE9FD; font-style: italic">int</span><span style="color: #F8F8F2">(data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">conf</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">][i]) </span><span style="color: #FF79C6">></span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">40</span><span style="color: #F8F8F2">:</span></span>
<span class="line"><span style="color: #F8F8F2">            x, y, w, h </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> (data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">left</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">][i], data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">top</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">][i],</span></span>
<span class="line"><span style="color: #F8F8F2">                          data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">width</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">][i], data[</span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">height</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2">][i])</span></span>
<span class="line"><span style="color: #F8F8F2">            arr[y</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2">y</span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2">h, x</span><span style="color: #FF79C6">:</span><span style="color: #F8F8F2">x</span><span style="color: #FF79C6">+</span><span style="color: #F8F8F2">w] </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> arr.min()   </span><span style="color: #6272A4"># blackout</span></span>
<span class="line"><span style="color: #F8F8F2">            redacted </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #BD93F9">True</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">if</span><span style="color: #F8F8F2"> redacted:</span></span>
<span class="line"><span style="color: #F8F8F2">        ds.PixelData </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> arr.tobytes()</span></span>
<span class="line"><span style="color: #F8F8F2">        ds.BurnedInAnnotation </span><span style="color: #FF79C6">=</span><span style="color: #F8F8F2"> </span><span style="color: #E9F284">"</span><span style="color: #F1FA8C">NO</span><span style="color: #E9F284">"</span></span>
<span class="line"><span style="color: #F8F8F2">    </span><span style="color: #FF79C6">return</span><span style="color: #F8F8F2"> ds</span></span>
<span class="line"></span>
```

This is a first pass, not a guarantee. OCR misses stylized fonts and low-contrast overlays. Human review of a sample is not optional for anything you publish.

### 6. Defacing and format conversion, at the edge of a header pipeline

For head-inclusive volumes, chain a defacing step (`pydeface`, `mri_deface`, or `afni 3dSkullStrip`) before release, and treat DICOM to NIfTI conversion as its own auditable stage:

# dcm2niix: convert, and crucially, anonymize the BIDS JSON sidecar too
dcm2niix -ba y -f "%i_%p" -o ./nifti_out ./dicom_in
# -ba y  : anonymize BIDS sidecar (strip patient/date fields)
# then defacing on the volume itself:
pydeface ./nifti_out/sub01.nii.gz --outfile ./nifti_out/sub01_defaced.nii.gz
```
<span class="line"><span style="color: #6272A4"># dcm2niix: convert, and crucially, anonymize the BIDS JSON sidecar too</span></span>
<span class="line"><span style="color: #F8F8F2">dcm2niix </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2">ba y </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2">f </span><span style="color: #E9F284">"</span><span style="color: #BD93F9">%i</span><span style="color: #F1FA8C">_%p</span><span style="color: #E9F284">"</span><span style="color: #F8F8F2"> </span><span style="color: #FF79C6">-</span><span style="color: #F8F8F2">o .</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">nifti_out .</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">dicom_in</span></span>
<span class="line"><span style="color: #6272A4"># -ba y  : anonymize BIDS sidecar (strip patient/date fields)</span></span>
<span class="line"><span style="color: #6272A4"># then defacing on the volume itself:</span></span>
<span class="line"><span style="color: #F8F8F2">pydeface .</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">nifti_out</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">sub01.nii.gz </span><span style="color: #FF5555; font-style: italic; text-decoration: underline">--</span><span style="color: #F8F8F2">outfile .</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">nifti_out</span><span style="color: #FF79C6">/</span><span style="color: #F8F8F2">sub01_defaced.nii.gz</span></span>
<span class="line"></span>
```

The `-ba y` flag is the one people forget. Without it, the JSON sidecar undoes your header work.

### A note on not building this yourself

For production, lean on tools that encode the standard’s intent. The Stanford **`deid`** library ships editable recipes mapping directly to PS3.15 actions. RSNA’s **CTP** (Clinical Trial Processor) is the reference pipeline for multicentre trials. **`dcm4che`** and **GDCM** provide battle-tested command-line anonymizers. Aryanto and colleagues tested ten free toolkits and found that, with default settings, only one removed every required element. Defaults lie. Configure explicitly, then verify.

---

## Verification: the step everyone skips

Would you trust a de-identification pipeline you never audited against an adversary? A defensible workflow closes with a re-identification attempt, not a checkbox. Diff the output header against a known identifier list. Re-run OCR on a pixel sample. For head imaging, attempt a face reconstruction on a handful of cases and see whether it renders a usable surface. The 2015 tooling comparison and the 2019 face-recognition study exist precisely because the “it looked clean” assumption keeps failing in the literature.

De-identification is not a filter you run once. It is a risk position you defend.

---

## Key takeaways

- Start by auditing, not scrubbing. Dump and triage the real content of files from every scanner you draw from, recursing into sequences, before you write a tag list.
- Blanking the patient name is roughly 5% of the job. PHI hides in dates, UIDs, private tags, structured content, the file preamble, and the pixels themselves.
- Use the DICOM PS3.15 Annex E Basic Profile plus explicit options as your framework, and declare conformance with `PatientIdentityRemoved` and a `DeidentificationMethodCodeSequence`.
- Remap UIDs _consistently_ and shift dates _consistently per patient_ to preserve analytic value without leaking identity.
- Headers cannot protect faces. Brain and head-inclusive volumes need defacing or skull-stripping; even chest X-rays carry biometric re-identification risk.
- NIfTI is not an anonymizer. It drops metadata by omission, but the face stays in the voxels and identifiers migrate to sidecars and filenames.
- Anonymization versus pseudonymization is a governance decision about the key, not a code difference. Know which one you are actually doing.
- Never trust default settings. Configure, then attempt re-identification before you release anything.

---

## References

1. NEMA. _DICOM PS3.15: Security and System Management Profiles, Annex E, Attribute Confidentiality Profiles._ [dicom.nema.org](https://dicom.nema.org/medical/dicom/current/output/chtml/part15/chapter_e.html)
2. Schwarz CG, Kremers WK, Therneau TM, et al. Identification of Anonymous MRI Research Participants with Face-Recognition Software. _N Engl J Med._ 2019;381(17):1684–1686. [doi:10.1056/NEJMc1908881](https://www.nejm.org/doi/full/10.1056/NEJMc1908881)
3. Packhäuser K, Gündel S, Münster N, et al. Deep learning-based patient re-identification is able to exploit the biometric nature of medical chest X-ray data. _Sci Rep._ 2022;12:14851. [doi:10.1038/s41598-022-19045-3](https://www.nature.com/articles/s41598-022-19045-3)
4. Aryanto KYE, Oudkerk M, van Ooijen PMA. Free DICOM de-identification tools in clinical research: functioning and safety of patient privacy. _Eur Radiol._ 2015;25(12):3685–3695. [doi:10.1007/s00330-015-3794-0](https://link.springer.com/article/10.1007/s00330-015-3794-0)
5. Clunie DA. Letter: Free DICOM de-identification tools in clinical research: functioning and safety of patient privacy. _European Radiology_, Opinions section (online correspondence), 20 April 2016. [journals.myesr.org](https://journals.myesr.org/eur-radiol/opinions/clunie-2016/)
6. Aryanto KYE, Oudkerk M, van Ooijen PMA. Reply to: Free DICOM de-identification tools in clinical research. _European Radiology_, Opinions section (online correspondence), 2016. [journals.myesr.org](https://journals.myesr.org/eur-radiol/opinions/reply-clunie-2016/)
7. Moore SM, Maffitt DR, Smith KE, et al. De-identification of medical images with retention of scientific research value. _RadioGraphics._ 2015;35(3):727–735.
8. Larobina M, Murino L. Medical image file formats. _J Digit Imaging._ 2014;27(2):200–206. [doi:10.1007/s10278-013-9657-9](https://link.springer.com/article/10.1007/s10278-013-9657-9)
9. Bischoff-Grethe A, Ozyurt IB, Busa E, et al. A technique for the de-identification of structural brain MR images. _Hum Brain Mapp._ 2007;28(9):892–903.
10. pydicom documentation: de-identification and anonymization. [pydicom.github.io](https://pydicom.github.io/)
11. Stanford `deid`: DICOM de-identification with editable recipes. [github.com/pydicom/deid](https://github.com/pydicom/deid)
12. RSNA Clinical Trial Processor (CTP). [mircwiki.rsna.org](https://mircwiki.rsna.org/index.php?title=CTP-The_RSNA_Clinical_Trial_Processor)
13. Rorden C. `dcm2niix`: DICOM to NIfTI conversion. [github.com/rordenlab/dcm2niix](https://github.com/rordenlab/dcm2niix)