Current section
Files
Jump to
Current section
Files
lib/dicom/de_identification.ex
defmodule Dicom.DeIdentification do
@moduledoc """
DICOM De-identification / Anonymization (PS3.15 Table E.1-1).
Implements a best-effort Basic Application Level Confidentiality Profile
for the supported tag set, with 10 profile flags that affect behavior.
Supports action codes D, Z, X, K, C, and U.
`retain_private_tags` retains all private tags unconditionally.
`retain_safe_private` implements PS3.15 Annex E.3.10: only private blocks
whose creator element value matches a known safe creator are retained;
all other private tags are removed.
When multiple profile flags overlap, the implementation prefers the more
conservative override. For temporal tags, `retain_long_full_dates` wins over
`retain_long_modified_dates`.
## Action Codes
- **D** ā Replace with dummy value (per VR)
- **Z** ā Replace with zero-length value
- **X** ā Remove the element
- **K** ā Keep (no change)
- **C** ā Clean (remove identifying text from descriptions)
- **U** ā Replace UID with consistent new UID
## Usage
{:ok, deidentified, uid_map} = Dicom.DeIdentification.apply(data_set)
# With options
profile = %Dicom.DeIdentification.Profile{retain_uids: true}
{:ok, result, uid_map} = Dicom.DeIdentification.apply(data_set, profile: profile)
# Direct flags are also accepted
{:ok, result, uid_map} =
Dicom.DeIdentification.apply(data_set, retain_uids: true, retain_private_tags: true)
Reference: DICOM PS3.15 Annex E.
"""
alias Dicom.{DataSet, DataElement, Tag, UID}
import Bitwise, only: [>>>: 2, &&&: 2]
# PS3.15 Annex E.3.10 -- well-known private creators whose blocks are
# considered safe (no PHI). When `retain_safe_private: true`, only private
# tags whose creator element value is in this list are retained.
@safe_private_creators MapSet.new([
"GEMS_GENIE_1",
"GEMS_ACQU_01",
"GEMS_IMAG_01",
"GEMS_SENO_02",
"GEMS_DL_FRAME_01",
"GEMS_PARM_01",
"GEMS_STDY_01",
"GEMS_SERS_01",
"SIEMENS MR HEADER",
"SIEMENS CT VA0 CMSINFO",
"SIEMENS CT VA0 GEN",
"SIEMENS MED",
"SIEMENS MED DISPLAY",
"SIEMENS MED NM",
"SIEMENS MED PT",
"SIEMENS CSA HEADER",
"SIEMENS CSA NON-IMAGE",
"Philips MR Imaging DD 001",
"Philips MR Imaging DD 002",
"Philips MR Imaging DD 003",
"Philips MR Imaging DD 004",
"Philips MR Imaging DD 005",
"Philips Imaging DD 001",
"Philips Imaging DD 002",
"PHILIPS MR SPECTRO;1",
"ELSCINT1",
"TOSHIBA_MEC_CT_01",
"TOSHIBA_MEC_MR3_01",
"CANON_MEC_CT_01",
"AGFA",
"AGFA-AG_HPState",
"FUJI FILM",
"FUJIFILM",
"Hologic",
"MITRA OBJECT UTF8 ATTRIBUTES 1.0",
"dcm4che/archive"
])
# CID 7050 (PS3.16) -- de-identification method code values.
@cid_7050_codes %{
basic_profile: {"113100", "DCM", "Basic Application Level Confidentiality Profile"},
clean_pixel_data: {"113101", "DCM", "Clean Pixel Data Option"},
clean_recognizable_visual_features:
{"113102", "DCM", "Clean Recognizable Visual Features Option"},
clean_graphics: {"113103", "DCM", "Clean Graphics Option"},
clean_structured_content: {"113104", "DCM", "Clean Structured Content Option"},
clean_descriptions: {"113105", "DCM", "Clean Descriptors Option"},
retain_long_full_dates:
{"113106", "DCM", "Retain Longitudinal Temporal Information Full Dates Option"},
retain_long_modified_dates:
{"113107", "DCM", "Retain Longitudinal Temporal Information Modified Dates Option"},
retain_patient_characteristics: {"113108", "DCM", "Retain Patient Characteristics Option"},
retain_device_identity: {"113109", "DCM", "Retain Device Identity Option"},
retain_uids: {"113110", "DCM", "Retain UIDs"},
retain_safe_private: {"113111", "DCM", "Retain Safe Private Option"},
retain_institution_identity: {"113112", "DCM", "Retain Institution Identity Option"}
}
@doc """
Returns the set of known safe private creators (PS3.15 Annex E.3.10).
"""
@spec safe_private_creators() :: MapSet.t(String.t())
def safe_private_creators, do: @safe_private_creators
@doc """
Returns the default de-identification profile.
"""
@spec basic_profile() :: __MODULE__.Profile.t()
def basic_profile, do: %__MODULE__.Profile{}
@doc """
Applies de-identification to a data set.
Returns `{:ok, deidentified_data_set, uid_map}` where `uid_map` maps
original UIDs to their replacements.
## Options
- `profile` ā a `DeIdentification.Profile` struct (default: `basic_profile()`)
- direct boolean profile flags such as `retain_uids: true` or
`retain_private_tags: true`; these override the supplied `profile`, if any
"""
@spec apply(DataSet.t(), keyword()) :: {:ok, DataSet.t(), map()}
def apply(%DataSet{} = ds, opts \\ []) do
profile = merge_profile_options(opts)
uid_map = %{}
{ds, uid_map} = process_elements(ds, profile, uid_map)
ds = strip_private_tags(ds, profile)
ds = add_deidentification_markers(ds, profile)
{:ok, ds, uid_map}
end
defp merge_profile_options(opts) do
base_profile = Keyword.get(opts, :profile, basic_profile())
profile_fields = Map.keys(base_profile) -- [:__struct__]
overrides =
opts
|> Keyword.drop([:profile])
|> Keyword.take(profile_fields)
|> Enum.into(%{})
struct(base_profile, overrides)
end
@doc """
Returns the action code for a tag given a profile.
"""
@spec action_for(Tag.t(), __MODULE__.Profile.t()) :: :D | :Z | :X | :K | :C | :U | :M
def action_for(tag, %__MODULE__.Profile{} = profile) do
tag
|> tag_action()
|> apply_profile_overrides(tag, profile)
end
# āā Tag ā Action mapping (PS3.15 Table E.1-1) āāāāāāāāāāāāāāāā
# D = dummy, Z = zero, X = remove, K = keep, U = replace UID
# X_or_C = remove by default, clean if clean_descriptions option
#
# For compound actions (X/Z, Z/D, X/Z/D, X/D):
# Z/D ā D (more informative for downstream systems)
# X/Z ā X (more conservative)
# X/D ā X (more conservative)
# X/Z/D ā X (more conservative)
# āā Patient identifying āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0010, 0x0010}), do: :D
defp tag_action({0x0010, 0x0020}), do: :Z
defp tag_action({0x0010, 0x0030}), do: :Z
defp tag_action({0x0010, 0x0032}), do: :X
defp tag_action({0x0010, 0x0040}), do: :Z
defp tag_action({0x0010, 0x1010}), do: :X
defp tag_action({0x0010, 0x1020}), do: :X
defp tag_action({0x0010, 0x1030}), do: :X
defp tag_action({0x0010, 0x1000}), do: :X
defp tag_action({0x0010, 0x1001}), do: :X
defp tag_action({0x0010, 0x1002}), do: :X
defp tag_action({0x0010, 0x1005}), do: :X
defp tag_action({0x0010, 0x1040}), do: :X
defp tag_action({0x0010, 0x1050}), do: :X
defp tag_action({0x0010, 0x1060}), do: :X
defp tag_action({0x0010, 0x1080}), do: :X
defp tag_action({0x0010, 0x1081}), do: :X
defp tag_action({0x0010, 0x1090}), do: :X
defp tag_action({0x0010, 0x2000}), do: :X
defp tag_action({0x0010, 0x2110}), do: :X
defp tag_action({0x0010, 0x2150}), do: :X
defp tag_action({0x0010, 0x2152}), do: :X
defp tag_action({0x0010, 0x2154}), do: :X
defp tag_action({0x0010, 0x2155}), do: :X
defp tag_action({0x0010, 0x2160}), do: :X
defp tag_action({0x0010, 0x2180}), do: :X
defp tag_action({0x0010, 0x21A0}), do: :X
defp tag_action({0x0010, 0x21B0}), do: :X
defp tag_action({0x0010, 0x21C0}), do: :X
defp tag_action({0x0010, 0x21D0}), do: :X
defp tag_action({0x0010, 0x21F0}), do: :X
defp tag_action({0x0010, 0x2203}), do: :X
defp tag_action({0x0010, 0x2297}), do: :X
defp tag_action({0x0010, 0x2299}), do: :X
defp tag_action({0x0010, 0x4000}), do: :X
defp tag_action({0x0010, 0x0050}), do: :X
defp tag_action({0x0010, 0x1100}), do: :X
# āā Study/Series identifying āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0008, 0x0050}), do: :Z
defp tag_action({0x0008, 0x0090}), do: :Z
defp tag_action({0x0008, 0x0092}), do: :X
defp tag_action({0x0008, 0x0094}), do: :X
defp tag_action({0x0008, 0x0096}), do: :X
defp tag_action({0x0008, 0x009C}), do: :Z
defp tag_action({0x0008, 0x009D}), do: :X
defp tag_action({0x0008, 0x0080}), do: :X
defp tag_action({0x0008, 0x0081}), do: :X
defp tag_action({0x0008, 0x0082}), do: :X
defp tag_action({0x0008, 0x1010}), do: :X
defp tag_action({0x0008, 0x1040}), do: :X
defp tag_action({0x0008, 0x1041}), do: :X
defp tag_action({0x0008, 0x1048}), do: :X
defp tag_action({0x0008, 0x1049}), do: :X
defp tag_action({0x0008, 0x1050}), do: :X
defp tag_action({0x0008, 0x1052}), do: :X
defp tag_action({0x0008, 0x1060}), do: :X
defp tag_action({0x0008, 0x1062}), do: :X
defp tag_action({0x0008, 0x1070}), do: :X
defp tag_action({0x0008, 0x1072}), do: :X
defp tag_action({0x0008, 0x4000}), do: :X
# āā Descriptions (X or C depending on profile) āāāāāāāāāāāāāā
defp tag_action({0x0008, 0x1030}), do: :X_or_C
defp tag_action({0x0008, 0x103E}), do: :X_or_C
defp tag_action({0x0008, 0x1090}), do: :X_or_C
defp tag_action({0x0008, 0x2111}), do: :X
defp tag_action({0x0020, 0x4000}), do: :X_or_C
defp tag_action({0x0028, 0x4000}), do: :X
defp tag_action({0x0040, 0x2400}), do: :X
# āā UIDs ā replace with consistent mapping āāāāāāāāāāāāāāāāāā
defp tag_action({0x0008, 0x0014}), do: :U
defp tag_action({0x0008, 0x0017}), do: :U
defp tag_action({0x0008, 0x0018}), do: :U
defp tag_action({0x0008, 0x0058}), do: :U
defp tag_action({0x0008, 0x1150}), do: :U
defp tag_action({0x0008, 0x1155}), do: :U
defp tag_action({0x0008, 0x1195}), do: :U
defp tag_action({0x0008, 0x3010}), do: :U
defp tag_action({0x0008, 0x0016}), do: :K
defp tag_action({0x0002, 0x0003}), do: :U
defp tag_action({0x0004, 0x1511}), do: :U
defp tag_action({0x0018, 0x1002}), do: :U
defp tag_action({0x0018, 0x100B}), do: :U
defp tag_action({0x0018, 0x2042}), do: :U
defp tag_action({0x0020, 0x000D}), do: :U
defp tag_action({0x0020, 0x000E}), do: :U
defp tag_action({0x0020, 0x0052}), do: :U
defp tag_action({0x0020, 0x0200}), do: :U
defp tag_action({0x0020, 0x9161}), do: :U
defp tag_action({0x0020, 0x9164}), do: :U
defp tag_action({0x0028, 0x1199}), do: :U
defp tag_action({0x0028, 0x1214}), do: :U
defp tag_action({0x003A, 0x0310}), do: :U
defp tag_action({0x0040, 0x0554}), do: :U
defp tag_action({0x0040, 0x4023}), do: :U
defp tag_action({0x0040, 0xA124}), do: :U
defp tag_action({0x0040, 0xA171}), do: :U
defp tag_action({0x0040, 0xA402}), do: :U
defp tag_action({0x0040, 0xDB0C}), do: :U
defp tag_action({0x0040, 0xDB0D}), do: :U
defp tag_action({0x0062, 0x0021}), do: :U
defp tag_action({0x0064, 0x0003}), do: :U
defp tag_action({0x0070, 0x031A}), do: :U
defp tag_action({0x0070, 0x1101}), do: :U
defp tag_action({0x0070, 0x1102}), do: :U
defp tag_action({0x0088, 0x0140}), do: :U
defp tag_action({0x0400, 0x0100}), do: :U
defp tag_action({0x3006, 0x0024}), do: :U
defp tag_action({0x3006, 0x00C2}), do: :U
defp tag_action({0x300A, 0x0013}), do: :U
defp tag_action({0x300A, 0x0054}), do: :U
defp tag_action({0x300A, 0x0609}), do: :U
defp tag_action({0x300A, 0x0650}), do: :U
defp tag_action({0x300A, 0x0700}), do: :U
defp tag_action({0x3010, 0x0006}), do: :U
defp tag_action({0x3010, 0x000B}), do: :U
defp tag_action({0x3010, 0x0013}), do: :U
defp tag_action({0x3010, 0x0015}), do: :U
defp tag_action({0x3010, 0x003B}), do: :U
defp tag_action({0x3010, 0x006E}), do: :U
defp tag_action({0x3010, 0x006F}), do: :U
# āā Content creator/observer āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0070, 0x0084}), do: :D
defp tag_action({0x0070, 0x0086}), do: :X
defp tag_action({0x0040, 0xA123}), do: :D
defp tag_action({0x0040, 0xA160}), do: :X
defp tag_action({0x0040, 0xA730}), do: :X
defp tag_action({0x0040, 0x1101}), do: :D
defp tag_action({0x0040, 0xA075}), do: :D
defp tag_action({0x0040, 0xA073}), do: :D
defp tag_action({0x0040, 0xA027}), do: :D
defp tag_action({0x0040, 0xA088}), do: :Z
defp tag_action({0x0040, 0xA030}), do: :D
defp tag_action({0x0040, 0xA078}), do: :X
defp tag_action({0x0040, 0xA082}), do: :Z
defp tag_action({0x0040, 0xA07A}), do: :X
# āā Keep: structural/non-identifying āāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0008, 0x0060}), do: :K
defp tag_action({0x0008, 0x0008}), do: :K
defp tag_action({0x0020, 0x0013}), do: :K
defp tag_action({0x0020, 0x0011}), do: :K
defp tag_action({0x0020, 0x0010}), do: :Z
defp tag_action({0x0020, 0x0032}), do: :K
defp tag_action({0x0020, 0x0037}), do: :K
defp tag_action({0x0020, 0x1041}), do: :K
# Group 0028 (image parameters) ā keep, except UIDs/comments handled above
defp tag_action({0x0028, _}), do: :K
# Group 7FE0 (pixel data) ā keep
defp tag_action({0x7FE0, _}), do: :K
# āā Device identifiers in group 0018 (before wildcard) āāāāāā
defp tag_action({0x0018, 0x1000}), do: :X
defp tag_action({0x0018, 0x1004}), do: :X
defp tag_action({0x0018, 0x1005}), do: :X
defp tag_action({0x0018, 0x1007}), do: :X
defp tag_action({0x0018, 0x1008}), do: :X
defp tag_action({0x0018, 0x1009}), do: :X
defp tag_action({0x0018, 0x100A}), do: :X
defp tag_action({0x0018, 0x1010}), do: :X
defp tag_action({0x0018, 0x1011}), do: :X
defp tag_action({0x0018, 0x1200}), do: :X
defp tag_action({0x0018, 0x1201}), do: :X
defp tag_action({0x0018, 0x1203}), do: :Z
defp tag_action({0x0018, 0x1400}), do: :X
defp tag_action({0x0018, 0x4000}), do: :X
defp tag_action({0x0018, 0x9424}), do: :X
defp tag_action({0x0018, 0x0010}), do: :D
defp tag_action({0x0018, 0x0027}), do: :X
defp tag_action({0x0018, 0x0035}), do: :X
defp tag_action({0x0018, 0x1042}), do: :X
defp tag_action({0x0018, 0x1043}), do: :X
defp tag_action({0x0018, 0x1078}), do: :X
defp tag_action({0x0018, 0x1079}), do: :X
defp tag_action({0x0018, 0xA002}), do: :X
defp tag_action({0x0018, 0xA003}), do: :X
# Group 0018 wildcard ā keep remaining acquisition parameters
defp tag_action({0x0018, _}), do: :K
# āā File Meta: keep (except MediaStorageSOPInstanceUID ā U above) āā
defp tag_action({0x0002, _}), do: :K
# āā Clinical trial tags āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0012, 0x0010}), do: :D
defp tag_action({0x0012, 0x0020}), do: :D
defp tag_action({0x0012, 0x0021}), do: :Z
defp tag_action({0x0012, 0x0030}), do: :Z
defp tag_action({0x0012, 0x0031}), do: :Z
defp tag_action({0x0012, 0x0040}), do: :D
defp tag_action({0x0012, 0x0042}), do: :D
defp tag_action({0x0012, 0x0050}), do: :Z
defp tag_action({0x0012, 0x0051}), do: :X
defp tag_action({0x0012, 0x0060}), do: :Z
defp tag_action({0x0012, 0x0071}), do: :X
defp tag_action({0x0012, 0x0072}), do: :X
defp tag_action({0x0012, 0x0081}), do: :D
defp tag_action({0x0012, 0x0082}), do: :X
# De-identification markers: keep
defp tag_action({0x0012, 0x0062}), do: :K
defp tag_action({0x0012, 0x0063}), do: :K
defp tag_action({0x0012, 0x0064}), do: :K
defp tag_action({0x0012, _}), do: :X
# āā Dates and times āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0008, 0x0020}), do: :Z
defp tag_action({0x0008, 0x0021}), do: :X
defp tag_action({0x0008, 0x0022}), do: :X
defp tag_action({0x0008, 0x0023}), do: :D
defp tag_action({0x0008, 0x0030}), do: :Z
defp tag_action({0x0008, 0x0031}), do: :X
defp tag_action({0x0008, 0x0032}), do: :X
defp tag_action({0x0008, 0x0033}), do: :D
defp tag_action({0x0008, 0x002A}), do: :X
defp tag_action({0x0008, 0x0012}), do: :X
defp tag_action({0x0008, 0x0013}), do: :X
defp tag_action({0x0008, 0x0015}), do: :X
# āā Procedure/scheduling āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0032, _}), do: :X
defp tag_action({0x0038, _}), do: :X
defp tag_action({0x0040, 0x0006}), do: :X
defp tag_action({0x0040, 0x0007}), do: :X
defp tag_action({0x0040, 0x0241}), do: :X
defp tag_action({0x0040, 0x0242}), do: :X
defp tag_action({0x0040, 0x0243}), do: :X
defp tag_action({0x0040, 0x0244}), do: :X
defp tag_action({0x0040, 0x0245}), do: :X
defp tag_action({0x0040, 0x0250}), do: :X
defp tag_action({0x0040, 0x0251}), do: :X
defp tag_action({0x0040, 0x0254}), do: :X
defp tag_action({0x0040, 0x0275}), do: :X
defp tag_action({0x0040, 0x0280}), do: :X
defp tag_action({0x0040, 0x0310}), do: :X
defp tag_action({0x0040, 0x1001}), do: :X
defp tag_action({0x0040, 0x1010}), do: :X
defp tag_action({0x0040, 0x1400}), do: :X
defp tag_action({0x0040, 0x2001}), do: :X
defp tag_action({0x0040, 0x2016}), do: :Z
defp tag_action({0x0040, 0x2017}), do: :Z
# āā Digital signatures āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0xFFFA, 0xFFFA}), do: :X
defp tag_action({0x0400, 0x0310}), do: :X
defp tag_action({0x0400, 0x0402}), do: :X
defp tag_action({0x0400, 0x0403}), do: :X
defp tag_action({0x0400, 0x0404}), do: :X
defp tag_action({0x0400, 0x0550}), do: :X
defp tag_action({0x0400, 0x0561}), do: :X
defp tag_action({0x0400, 0x0115}), do: :D
defp tag_action({0x0400, 0x0105}), do: :D
defp tag_action({0x0400, 0x0562}), do: :D
defp tag_action({0x0400, 0x0563}), do: :D
defp tag_action({0x0400, 0x0565}), do: :D
# āā Graphics/presentation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0070, 0x0001}), do: :D
defp tag_action({0x0070, 0x0006}), do: :D
defp tag_action({0x0070, 0x0008}), do: :X
defp tag_action({0x0070, 0x0082}), do: :X
defp tag_action({0x0070, 0x0083}), do: :X
# āā Interpretation (retired) āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x4008, _}), do: :X
# āā Overlay/curve data āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({g, _}) when g >= 0x5000 and g <= 0x50FF, do: :X
defp tag_action({g, 0x4000}) when g >= 0x6000 and g <= 0x60FF, do: :X
defp tag_action({g, 0x3000}) when g >= 0x6000 and g <= 0x60FF, do: :X
# āā Radiotherapy āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x300A, 0x0002}), do: :D
defp tag_action({0x300A, 0x0003}), do: :X
defp tag_action({0x300A, 0x0004}), do: :X
defp tag_action({0x300A, 0x0006}), do: :X
defp tag_action({0x300A, 0x0007}), do: :X
defp tag_action({0x300A, 0x000E}), do: :X
defp tag_action({0x300A, 0x0016}), do: :X
defp tag_action({0x300A, 0x00C3}), do: :X
defp tag_action({0x3006, 0x0002}), do: :D
defp tag_action({0x3006, 0x0004}), do: :X
defp tag_action({0x3006, 0x0006}), do: :X
defp tag_action({0x3006, 0x0008}), do: :Z
defp tag_action({0x3006, 0x0009}), do: :Z
defp tag_action({0x3006, 0x0026}), do: :Z
defp tag_action({0x3006, 0x0028}), do: :X
defp tag_action({0x3006, 0x0038}), do: :X
defp tag_action({0x3006, 0x0085}), do: :X
defp tag_action({0x3006, 0x0088}), do: :X
defp tag_action({0x3006, 0x00A6}), do: :Z
defp tag_action({0x3008, 0x0054}), do: :X
defp tag_action({0x3008, 0x0056}), do: :X
defp tag_action({0x3008, 0x0250}), do: :X
defp tag_action({0x3008, 0x0251}), do: :X
defp tag_action({0x300E, 0x0004}), do: :Z
defp tag_action({0x300E, 0x0005}), do: :Z
defp tag_action({0x300E, 0x0008}), do: :X
# āā Specimen āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0040, 0x050A}), do: :X
defp tag_action({0x0040, 0x0512}), do: :D
defp tag_action({0x0040, 0x0513}), do: :Z
defp tag_action({0x0040, 0x051A}), do: :X
defp tag_action({0x0040, 0x0551}), do: :D
defp tag_action({0x0040, 0x0562}), do: :Z
defp tag_action({0x0040, 0x0600}), do: :X
defp tag_action({0x0040, 0x0602}), do: :X
defp tag_action({0x0040, 0x0610}), do: :Z
# āā Referenced sequences āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0x0008, 0x1110}), do: :X
defp tag_action({0x0008, 0x1111}), do: :X
defp tag_action({0x0008, 0x1120}), do: :X
defp tag_action({0x0008, 0x1140}), do: :X
# āā Trailing padding āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action({0xFFFC, 0xFFFC}), do: :X
# āā Default: remove unknown āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp tag_action(_), do: :X
# āā Processing pipeline āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp process_elements(%DataSet{} = ds, profile, uid_map) do
{new_file_meta, uid_map} = process_element_map(ds.file_meta, profile, uid_map)
{new_elements, uid_map} = process_element_map(ds.elements, profile, uid_map)
{%{ds | file_meta: new_file_meta, elements: new_elements}, uid_map}
end
defp process_element_map(elements, profile, uid_map) do
Enum.reduce(elements, {%{}, uid_map}, fn {tag, elem}, {acc, umap} ->
action = action_for(tag, profile)
{elem, umap} = recurse_sequence(elem, profile, umap)
{new_elem, umap} = apply_action(action, elem, profile, umap)
case new_elem do
nil -> {acc, umap}
elem -> {Map.put(acc, tag, elem), umap}
end
end)
end
# Recurse into SQ elements before applying the parent action.
# PS3.15 requires de-identification of nested items regardless of the
# sequence tag's own action code.
defp recurse_sequence(%DataElement{vr: :SQ, value: items} = elem, profile, uid_map)
when is_list(items) do
{new_items, uid_map} = deidentify_sequence(items, profile, uid_map)
{%{elem | value: new_items}, uid_map}
end
defp recurse_sequence(elem, _profile, uid_map), do: {elem, uid_map}
defp apply_action(:D, %DataElement{} = elem, _profile, uid_map) do
dummy = dummy_value(elem.vr)
{%{elem | value: dummy, length: byte_size(dummy)}, uid_map}
end
defp apply_action(:Z, %DataElement{} = elem, _profile, uid_map) do
{%{elem | value: "", length: 0}, uid_map}
end
defp apply_action(:X, _elem, _profile, uid_map) do
{nil, uid_map}
end
defp apply_action(:K, elem, _profile, uid_map) do
{elem, uid_map}
end
defp apply_action(:C, %DataElement{} = elem, _profile, uid_map) do
cleaned = "CLEANED"
{%{elem | value: cleaned, length: byte_size(cleaned)}, uid_map}
end
defp apply_action(:U, %DataElement{value: value} = elem, _profile, uid_map)
when is_binary(value) do
uid = String.trim_trailing(value, <<0>>)
{new_uid, uid_map} =
case Map.get(uid_map, uid) do
nil ->
generated = UID.generate()
{generated, Map.put(uid_map, uid, generated)}
existing ->
{existing, uid_map}
end
{%{elem | value: new_uid, length: byte_size(new_uid)}, uid_map}
end
defp apply_action(:U, elem, _profile, uid_map) do
{elem, uid_map}
end
defp apply_action(:M, %DataElement{} = elem, _profile, uid_map) do
modified = modify_temporal_value(elem.value, elem.vr)
{%{elem | value: modified, length: byte_size(modified)}, uid_map}
end
defp deidentify_sequence(items, profile, uid_map) do
Enum.map_reduce(items, uid_map, fn item, umap ->
{new_item, umap} =
Enum.reduce(item, {%{}, umap}, fn {tag, elem}, {acc, umap} ->
action = action_for(tag, profile)
{elem, umap} = recurse_sequence(elem, profile, umap)
{new_elem, umap} = apply_action(action, elem, profile, umap)
case new_elem do
nil -> {acc, umap}
elem -> {Map.put(acc, tag, elem), umap}
end
end)
{new_item, umap}
end)
end
defp strip_private_tags(%DataSet{} = ds, %__MODULE__.Profile{retain_private_tags: true}) do
ds
end
defp strip_private_tags(%DataSet{} = ds, %__MODULE__.Profile{retain_safe_private: true}) do
safe_blocks = safe_private_block_numbers(ds.elements)
filtered =
Map.filter(ds.elements, fn {tag, _} ->
not Tag.private?(tag) or private_tag_in_safe_block?(tag, safe_blocks)
end)
%{ds | elements: filtered}
end
defp strip_private_tags(%DataSet{} = ds, _profile) do
%{ds | elements: Map.filter(ds.elements, fn {tag, _} -> not Tag.private?(tag) end)}
end
defp safe_private_block_numbers(elements) do
elements
|> Enum.reduce(%{}, fn
{{group, element}, %DataElement{value: creator}}, acc
when rem(group, 2) == 1 and element >= 0x0010 and element <= 0x00FF and
is_binary(creator) ->
trimmed = String.trim(creator)
if MapSet.member?(@safe_private_creators, trimmed) do
Map.put(acc, {group, element}, trimmed)
else
acc
end
_, acc ->
acc
end)
end
defp private_tag_in_safe_block?({group, element}, safe_blocks) when rem(group, 2) == 1 do
block_number = if element <= 0x00FF, do: element, else: (element &&& 0xFF00) >>> 8
Map.has_key?(safe_blocks, {group, block_number})
end
defp private_tag_in_safe_block?(_tag, _safe_blocks), do: false
defp add_deidentification_markers(%DataSet{} = ds, profile) do
ds
|> DataSet.put({0x0012, 0x0062}, :CS, patient_identity_removed_marker(profile))
|> DataSet.put({0x0012, 0x0063}, :LO, deidentification_method_marker(profile))
|> add_method_code_sequence(profile)
end
# (0012,0064) De-identification Method Code Sequence -- CID 7050 coded
# entries indicating which profile options were applied.
defp add_method_code_sequence(%DataSet{} = ds, profile) do
items = cid_7050_sequence_items(profile)
case items do
[] -> ds
_ -> DataSet.put(ds, {0x0012, 0x0064}, :SQ, items)
end
end
defp cid_7050_sequence_items(profile) do
base = [code_sequence_item(@cid_7050_codes[:basic_profile])]
option_items =
[
:clean_graphics,
:clean_structured_content,
:clean_descriptions,
:retain_long_full_dates,
:retain_long_modified_dates,
:retain_patient_characteristics,
:retain_device_identity,
:retain_uids,
:retain_safe_private,
:retain_institution_identity
]
|> Enum.filter(fn field -> Map.get(profile, field, false) end)
|> Enum.map(fn field -> code_sequence_item(@cid_7050_codes[field]) end)
base ++ option_items
end
defp code_sequence_item({code_value, scheme, meaning}) do
%{
{0x0008, 0x0100} => DataElement.new({0x0008, 0x0100}, :SH, code_value),
{0x0008, 0x0102} => DataElement.new({0x0008, 0x0102}, :SH, scheme),
{0x0008, 0x0104} => DataElement.new({0x0008, 0x0104}, :LO, meaning)
}
end
defp patient_identity_removed_marker(profile) do
if profile_retains_identity?(profile), do: "NO", else: "YES"
end
defp deidentification_method_marker(profile) do
case profile_option_labels(profile) do
[] ->
"Best-effort de-identification"
labels ->
Enum.join(["Best-effort de-identification" | labels], "\\")
end
end
defp profile_option_labels(profile) do
[
retain_uids: "retain_uids",
retain_device_identity: "retain_device_identity",
retain_patient_characteristics: "retain_patient_characteristics",
retain_institution_identity: "retain_institution_identity",
retain_long_full_dates: "retain_long_full_dates",
retain_long_modified_dates: "retain_long_modified_dates",
clean_descriptions: "clean_descriptions",
clean_structured_content: "clean_structured_content",
clean_graphics: "clean_graphics",
retain_private_tags: "retain_private_tags",
retain_safe_private: "retain_safe_private"
]
|> Enum.flat_map(fn {field, label} ->
if Map.get(profile, field), do: [label], else: []
end)
|> Enum.uniq()
end
# āā Dummy values per VR āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
defp dummy_value(:PN), do: "ANONYMOUS"
defp dummy_value(:DA), do: "19000101"
defp dummy_value(:TM), do: "000000"
defp dummy_value(:DT), do: "19000101000000.000000"
defp dummy_value(:LO), do: "ANONYMOUS"
defp dummy_value(:SH), do: "ANON"
defp dummy_value(:CS), do: "ANON"
defp dummy_value(:AS), do: "000Y"
defp dummy_value(:DS), do: "0"
defp dummy_value(:IS), do: "0"
defp dummy_value(:UI), do: UID.generate()
defp dummy_value(_), do: ""
defp apply_profile_overrides(:U, _tag, %__MODULE__.Profile{retain_uids: true}), do: :K
defp apply_profile_overrides(:X_or_C, _tag, %__MODULE__.Profile{clean_descriptions: true}),
do: :C
defp apply_profile_overrides(:X_or_C, _tag, _profile), do: :X
defp apply_profile_overrides(action, {group, _}, %__MODULE__.Profile{} = profile)
when rem(group, 2) == 1,
do: if(retain_private_tags?(profile), do: :K, else: action)
defp apply_profile_overrides(_action, tag, %__MODULE__.Profile{retain_device_identity: true})
when tag in [
{0x0008, 0x1010},
{0x0008, 0x1090},
{0x0018, 0x1000},
{0x0018, 0x1004},
{0x0018, 0x1005},
{0x0018, 0x1007},
{0x0018, 0x1008},
{0x0018, 0x1009},
{0x0018, 0x100A},
{0x0018, 0x1010},
{0x0018, 0x1011}
],
do: :K
defp apply_profile_overrides(
_action,
tag,
%__MODULE__.Profile{retain_patient_characteristics: true}
)
when tag in [{0x0010, 0x0040}, {0x0010, 0x1010}],
do: :K
defp apply_profile_overrides(
_action,
tag,
%__MODULE__.Profile{retain_institution_identity: true}
)
when tag in [{0x0008, 0x0080}, {0x0008, 0x0081}, {0x0008, 0x1040}],
do: :K
defp apply_profile_overrides(
_action,
tag,
%__MODULE__.Profile{clean_structured_content: true}
)
when tag in [{0x0040, 0xA730}, {0x0040, 0xA010}, {0x0040, 0xA040}, {0x0040, 0xA043}],
do: :K
defp apply_profile_overrides(
_action,
tag,
%__MODULE__.Profile{clean_structured_content: true}
)
when tag in [{0x0040, 0xA123}, {0x0040, 0xA124}, {0x0040, 0xA160}],
do: :C
defp apply_profile_overrides(_action, tag, %__MODULE__.Profile{clean_graphics: true})
when tag in [{0x0070, 0x0001}, {0x0070, 0x0008}],
do: :K
defp apply_profile_overrides(_action, tag, %__MODULE__.Profile{clean_graphics: true})
when tag == {0x0070, 0x0006},
do: :C
defp apply_profile_overrides(action, tag, %__MODULE__.Profile{} = profile) do
case temporal_override(profile, tag) do
nil -> action
override -> override
end
end
defp apply_profile_overrides(action, _tag, _profile), do: action
defp retain_private_tags?(%__MODULE__.Profile{
retain_private_tags: retain_private_tags,
retain_safe_private: retain_safe_private
}) do
retain_private_tags or retain_safe_private
end
defp profile_retains_identity?(%__MODULE__.Profile{} = profile) do
profile.retain_uids or
profile.retain_device_identity or
profile.retain_patient_characteristics or
profile.retain_institution_identity or
profile.retain_long_full_dates or
profile.retain_long_modified_dates or
retain_private_tags?(profile)
end
defp temporal_override(%__MODULE__.Profile{} = profile, tag) do
if temporal_tag?(tag), do: temporal_tag_override(profile), else: nil
end
defp temporal_tag_override(%__MODULE__.Profile{retain_long_full_dates: true}), do: :K
defp temporal_tag_override(%__MODULE__.Profile{retain_long_modified_dates: true}), do: :M
defp temporal_tag_override(%__MODULE__.Profile{}), do: nil
defp temporal_tag?(tag) do
tag in [
{0x0008, 0x0012},
{0x0008, 0x0013},
{0x0008, 0x0015},
{0x0008, 0x0020},
{0x0008, 0x0021},
{0x0008, 0x0022},
{0x0008, 0x0023},
{0x0008, 0x002A},
{0x0008, 0x0030},
{0x0008, 0x0031},
{0x0008, 0x0032},
{0x0008, 0x0033}
]
end
defp modify_temporal_value(value, vr) do
decoded =
case value do
binary when is_binary(binary) -> Dicom.Value.decode(binary, vr)
other -> other
end
case {vr, decoded} do
{:DA, date} when is_binary(date) -> shift_date_string(date)
{:DT, datetime} when is_binary(datetime) -> shift_datetime_string(datetime)
{:TM, time} when is_binary(time) -> String.trim(time)
_ -> ""
end
end
defp shift_date_string(date) do
shift_date_prefix(date)
end
defp shift_datetime_string(datetime) do
trimmed = String.trim(datetime)
if byte_size(trimmed) >= 8 do
<<date_part::binary-size(8), rest::binary>> = trimmed
shift_date_prefix(date_part) <> rest
else
trimmed
end
end
defp shift_date_prefix(date) do
trimmed = String.trim(date)
case Dicom.Value.to_date(trimmed) do
{:ok, parsed} ->
parsed
|> shift_date_years(-10)
|> Dicom.Value.from_date()
{:error, _} ->
trimmed
end
end
defp shift_date_years(%Date{year: year, month: month, day: day}, delta_years) do
target_year = year + delta_years
target_day = min(day, last_day_of_month(target_year, month))
{:ok, shifted} = Date.new(target_year, month, target_day)
shifted
end
defp last_day_of_month(year, month) do
{:ok, start_of_month} = Date.new(year, month, 1)
start_of_month |> Date.end_of_month() |> Map.fetch!(:day)
end
end