conda create -n phi-finder python==3.11
conda activate phi-finder
pip install -e .[dev,test] --no-cache-dir
pytest .python -m pip install --upgrade build
python -m build
pip install dist/phi_finder-0.1.18-py3-none-any.whlimport pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
anonymised_dcm = anonymise_dicom.anonymise_image(dcm)
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')import pydicom as dicom
from presidio_image_redactor import (
DicomImageRedactorEngine, ImageAnalyzerEngine, ContrastSegmentedImageEnhancer)
from phi_finder.dicom_tools import anonymise_dicom
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
score_threshold=.15
analyser = anonymise_dicom._build_presidio_analyser(score_threshold, "en_core_web_lg")
image_redactor = DicomImageRedactorEngine(
image_analyzer_engine=ImageAnalyzerEngine(
analyzer_engine=analyser,
image_preprocessor=ContrastSegmentedImageEnhancer(),
))
anonymised_dcm = anonymise_dicom.anonymise_image(dcm,score_threshold=score_threshold,
analyser=analyser,
image_redactor=image_redactor,
)
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')html_report builds a self-contained HTML report describing what was removed:
the names of the scrubbed header fields, plus a before/after diff of every
changed header value (e.g. a radiology report). The snapshot must be taken
before anonymise_image, which mutates the dataset in place.
import pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom, html_report
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
# Record the header values before they are redacted.
value_snapshot = html_report.snapshot_values(dcm)
gliner_pii = anonymise_dicom._build_transformer()
anonymised_dcm = anonymise_dicom.anonymise_image(dcm, gliner_pii=gliner_pii, use_case='dicom_retain_patient_scan_private', score_threshold=0.15)
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')
report = html_report.build_html_report(
html_report.read_flagged_headers(anonymised_dcm),
n_images=1,
session_id="my-session",
use_case="dicom_retain_patient_scan_private",
value_diffs=html_report.collect_value_diffs(value_snapshot, anonymised_dcm),
)
with open('/path/to/some/deidentification_report.html', 'w', encoding='utf-8') as f:
f.write(report)value_diffs is optional — omit it (along with snapshot_values /
collect_value_diffs) to get a report that lists only the names of the scrubbed
header fields.
Warning: a report built with
value_diffsreproduces the original clinical-note text, since the struck-through spans are the PHI itself.
A DICOM series is many files. To describe the whole session in a single report,
accumulate the findings across images and pass n_images as the total: extend
one flagged_headers list per image, and de-duplicate the value diffs by
html_report.diff_key(diff) so a field that is identical across every slice
(e.g. a repeated note) is shown once, while genuinely per-slice values (e.g. a
UID) are kept distinct.
import pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom, html_report
paths = ["/path/to/dicom0.dcm", "/path/to/dicom1.dcm"]
flagged_headers = []
value_diffs = {} # keyed by diff_key so duplicates across slices collapse
for i, path in enumerate(paths):
dcm = dicom.dcmread(path)
value_snapshot = html_report.snapshot_values(dcm) # before anonymise mutates
anonymised_dcm = anonymise_dicom.anonymise_image(dcm)
anonymised_dcm.save_as(f"/path/to/dicom{i}_anon.dcm")
flagged_headers.extend(html_report.read_flagged_headers(anonymised_dcm))
for diff in html_report.collect_value_diffs(value_snapshot, anonymised_dcm):
value_diffs.setdefault(html_report.diff_key(diff), diff)
report = html_report.build_html_report(
flagged_headers,
n_images=len(paths),
session_id="my-session",
use_case="Standard",
value_diffs=list(value_diffs.values()),
)
with open("/path/to/deidentification_report.html", "w", encoding="utf-8") as f:
f.write(report)The use_case argument selects how header values are de-identified:
use_case |
Standard headers | Private headers | Patient characteristics |
|---|---|---|---|
"Standard" (default), "Aggressive", or any other value |
Presidio + GLiNER NER redaction | Presidio + GLiNER NER redaction | Sex kept; age → 000Y; birth date → year |
"PS3.15" / "dicom_default" |
PS3.15 Basic Profile | Removed | Removed |
"PS3.15_Rtn. Pat." / "dicom_retain_patient" |
PS3.15 Basic Profile | Removed | Kept (age, sex, size, weight, …) |
"dicom_default_scan_private" |
PS3.15 Basic Profile | NER-scrubbed (kept) | Removed |
"dicom_retain_patient_scan_private" |
PS3.15 Basic Profile | NER-scrubbed (kept) | Kept (age, sex, size, weight, …) |
Matching is case-insensitive and separator-tolerant (see the note at the end of this section).
By default anonymise_image scans the header values with the Presidio NER
pipeline (and GLiNER, when supplied). Passing use_case="PS3.15" (or its
friendlier alias use_case="dicom_default") instead
de-identifies the headers with the DICOM
PS3.15 Annex E Basic Application Level Confidentiality Profile. In this mode the NER engines are not run on the headers.
import pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
anonymised_dcm = anonymise_dicom.anonymise_image(dcm, use_case="PS3.15")
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')Use use_case="PS3.15_Rtn. Pat." (or its friendlier alias
use_case="dicom_retain_patient") to apply the basic profile together with the
PS3.15 Retain Patient Characteristics Option. Direct identifiers (patient
name, birth date, etc.) are still removed, but patient characteristics such as
age, sex, size, weight, ethnic group and smoking status are kept.
import pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
anonymised_dcm = anonymise_dicom.anonymise_image(dcm, use_case="PS3.15_Rtn. Pat.")
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')Both DICOM modes have a _scan_private variant —
use_case="dicom_default_scan_private" and
use_case="dicom_retain_patient_scan_private". The standard headers are handled
exactly as in the matching profile above, but instead of removing private
attributes (the Basic Profile default), they are kept and their text values
are scanned with the Presidio/GLiNER pipeline.
import pydicom as dicom
from phi_finder.dicom_tools import anonymise_dicom
path = "/path/to/some/dicom.dcm"
dcm = dicom.dcmread(path)
anonymised_dcm = anonymise_dicom.anonymise_image(dcm, use_case="dicom_default_scan_private")
anonymised_dcm.save_as('/path/to/some/dicom_anon.dcm')The use_case match is case-insensitive and tolerant of separator spelling, so
"PS3.15", "ps3.15", "PS3_15", "PS3-15" and the alias "dicom_default"
all select the plain profile, and "PS3.15_Rtn. Pat.",
"PS3.15 Retain Patient Characteristics" or the alias "dicom_retain_patient"
select the retain variant. Appending _scan_private to either alias selects the
private-header-scanning variant. Any other value (e.g. "Standard",
or "NER Only") falls back to the Presidio/GLiNER pipeline.
Note:
use_caseonly controls how the headers are handled. Burned-in pixel PHI is still redacted only when animage_redactoris passed, exactly as in the examples above.