- name
- extracting-dicom-metadata
- description
- Reads DICOM file headers and DICOM-SR (Structured Report) content to pull study/series metadata and embedded report text, and flags PHI carried in header tags. Use before OpenMed processing when ingesting imaging data (CT/MR/CR/US, radiology SR) and you need the report narrative de-identified and analyzed, plus a list of header tags that must be scrubbed. Hand SR/report text to openmed.deidentify and openmed.analyze_text; use pydicom to read tags. Trigger keywords: DICOM, pydicom, DICOM-SR, structured report, PatientName, study metadata, PACS, radiology report, PS3.
- license
- Apache-2.0
- metadata
- {"project":"OpenMed","category":"data-ingestion","pairs":"before","version":"1.0"}
# Extracting DICOM Metadata & Report Text for OpenMed
DICOM (Digital Imaging and Communications in Medicine) files carry far more than
pixels: a **header** of tagged attributes (patient, study, series, equipment)
and, for **DICOM-SR (Structured Reports)**, a content tree holding the actual
radiology/cardiology *report text*. Two jobs sit here: pull the report narrative
for NLP, and **flag the PHI in the header** so it gets scrubbed. This skill does
both, then hands narrative to OpenMed. Header tags are read with `pydicom`
(external, MIT-licensed); de-identification of the extracted text is OpenMed's.
## When to use
- You ingest DICOM from PACS/VNA or a research archive and want the SR report
text mined with clinical NLP.
- You must enumerate PHI-bearing header tags before sharing/exporting images.
- You have DICOM-SR objects (e.g. radiology measurements + impression) whose
content tree contains the dictated report.
## DICOM headers in one minute
Every attribute has a **tag** `(gggg,eeee)` (group, element), a **VR** (value
representation, e.g. `PN` person name, `DA` date, `UI` UID), and a value. PHI
clusters in well-known tags:
| Tag | Name | VR | Notes |
| --- | --- | --- | --- |
| (0010,0010) | PatientName | PN | direct identifier |
| (0010,0020) | PatientID | LO | MRN |
| (0010,0030) | PatientBirthDate | DA | DOB |
| (0010,1040) | PatientAddress | LO | address |
| (0008,0090) | ReferringPhysicianName | PN | provider |
| (0008,0020/0030) | StudyDate / StudyTime | DA/TM | dates |
| (0008,0050) | AccessionNumber | SH | order id |
| (0008,103E) | SeriesDescription | LO | free text — may leak PHI |
| (0020,4000) | ImageComments | LT | free text — may leak PHI |
| (0040,A730) | ContentSequence | SQ | DICOM-SR report tree |
## Quick start
Read the header, pull SR report text, flag PHI tags, hand off to OpenMed:
```python
import pydicom
import openmed
ds = pydicom.dcmread("study.dcm")
# 1) Enumerate PHI-bearing header tags (report, do not log values).
PHI_TAGS = [
(0x0010, 0x0010), (0x0010, 0x0020), (0x0010, 0x0030), (0x0010, 0x1040),
(0x0008, 0x0090), (0x0008, 0x0050), (0x0008, 0x0020), (0x0008, 0x0030),
]
present_phi = [hex_pair for hex_pair in PHI_TAGS if hex_pair in ds]
# 2) Extract report text from a DICOM-SR content tree (recursively).
def sr_text(dataset):
chunks = []
for item in dataset.get("ContentSequence", []):
vt = item.get("ValueType")
if vt == "TEXT" and "TextValue" in item:
chunks.append(item.TextValue)
if "ContentSequence" in item: # nested CONTAINER
chunks.append(sr_text(item))
return "\n".join(c for c in chunks if c)
report = sr_text(ds)
# Some modalities stash narrative in free-text header tags too:
for tag in ("ImageComments", "SeriesDescription", "StudyDescription"):
if tag in ds and isinstance(ds.get(tag), str):
report += "\n" + ds.get(tag)
# 3) De-identify the narrative, then run NER.
if report.strip():
deid = openmed.deidentify(report, method="replace", policy="hipaa_safe_harbor")
result = openmed.analyze_text(deid.text, output_format="dict")
```
`pydicom` reads tags by keyword (`ds.PatientName`) or by `(group, element)`.
DICOM-SR text lives in the recursive `ContentSequence` content tree.
## Workflow
1. **Read the dataset** with `pydicom.dcmread` (use `stop_before_pixels=True`
for header-only/metadata work — faster, avoids loading pixels).
2. **Walk the SR content tree.** `ContentSequence` nests `CONTAINER`, `TEXT`,
`CODE`, `NUM`, `PNAME` nodes; concatenate `TEXT.TextValue` (and relevant
`CODE`/`NUM` measurements) in document order to reconstruct the report.
3. **Inventory PHI tags.** Flag the standard identifier tags *and* free-text
tags (`ImageComments`, `*Description`) that frequently leak PHI. Report tag
presence — never echo the values into logs.
4. **De-identify → analyze** the report narrative with OpenMed.
5. **Scrub the header** before any image export using a DICOM de-identification
profile (PS3.15 Annex E / Basic Application Level Confidentiality). OpenMed
de-identifies the *narrative*; header scrubbing is a separate DICOM step.
## Hand-off to / from OpenMed
- **To OpenMed:** SR report text (and free-text header tags) →
`openmed.deidentify` → `openmed.analyze_text`.
- **Header de-id is out of scope for OpenMed** — OpenMed handles the *text*
narrative; use a DICOM-native de-identifier (pydicom + PS3.15 profile, or a
PACS de-id node) to scrub `(0010,xxxx)` and burned-in-pixel PHI. This skill's
job is to flag those tags so they aren't missed.
- **Re-link by UID, not PHI.** Carry `StudyInstanceUID`/`SeriesInstanceUID` as
rejoin keys; these are not identifiers but should be re-mapped consistently if
the profile requires UID remapping.
## Edge cases & gotchas
- **Pixel-burned PHI.** Ultrasound and secondary-capture images often burn name/
MRN/date into the *pixels* — header scrubbing alone is insufficient; flag
modalities (US, SC, XC) for pixel review/OCR. OpenMed's multimodal/OCR intake
can read burned-in text for redaction screening.
- **Private tags.** Vendor `(gggg,eeee)` odd-group private tags can hide PHI;
PS3.15 requires removing or whitelisting them — don't trust unknown tags.
- **Date shifting must be consistent.** If you date-shift `StudyDate`, shift all
related dates by the same offset to preserve temporal relationships.
- **SR value types.** Not all SR content is narrative — `NUM` (measurements),
`CODE` (coded findings), `PNAME` (person names, PHI!) need different handling;
don't dump `PNAME` into NLP text.
- **Character sets.** Honor `SpecificCharacterSet (0008,0005)`; non-Latin
patient names need correct decoding before de-id.
- **Read-only intake.** Treat source DICOM as immutable; write de-identified
copies, never overwrite originals.
## Standards & references
- DICOM standard (PS3.x), Part 6 Data Dictionary (tags):
https://www.dicomstandard.org/current
- PS3.15 Annex E — Attribute Confidentiality Profiles (de-identification):
https://dicom.nema.org/medical/dicom/current/output/html/part15.html#chapter_E
- DICOM-SR (PS3.3 Structured Reporting; PS3.16 templates):
https://dicom.nema.org/medical/dicom/current/output/html/part03.html
- pydicom documentation: https://pydicom.github.io/
- DICOM PS3.16 TID 2000 (Basic Diagnostic Imaging Report):
https://dicom.nema.org/medical/dicom/current/output/html/part16.html
在 GitHub 查看