Dicom

Hex.pmDocsCILicense: MIT

██████╗ ██╗ ██████╗ ██████╗ ███╗   ███╗
██╔══██╗██║██╔════╝██╔═══██╗████╗ ████║
██║  ██║██║██║     ██║   ██║██╔████╔██║
██║  ██║██║██║     ██║   ██║██║╚██╔╝██║
██████╔╝██║╚██████╗╚██████╔╝██║ ╚═╝ ██║
╚═════╝ ╚═╝ ╚═════╝ ╚═════╝ ╚═╝     ╚═╝

  DICOM toolkit for Elixir · PS3.5/6/10/15/16/18

Pure Elixir DICOM toolkit for DICOM Part 10 files. Zero runtime dependencies.

Features

Installation

Add dicom to your mix.exs dependencies:

def deps do
  [
    {:dicom, "~> 0.9.1"}
  ]
end

Structured Reports

dicom includes complete PS3.16 SR authoring with all 33 root template builders:

Domain Templates
General Imaging MeasurementReport (1500), TranscribedDiagnosticImagingReport (2005), ImagingReport (2006), KeyObjectSelection (2010)
Ophthalmology SpectaclePrescriptionReport (2020), MacularGridReport (2100)
Procedure Log ProcedureLog (3001)
Cardiology IVUSReport (3250), StressTestingReport (3300), HemodynamicsReport (3500), ECGReport (3700), WaveformAnnotation (3750), CardiacCatheterizationReport (3800), CardiovascularAnalysisReport (3900)
CAD MammographyCAD (4000), ChestCAD (4100), ColonCAD (4120)
Breast/Prostate BreastImagingReport (4200, BI-RADS), ProstateMRReport (4300, PI-RADS)
Ultrasound OBGYNUltrasoundReport (5000), VascularUltrasoundReport (5100), EchocardiographyReport (5200), PediatricCardiacUSReport (5220), SimplifiedEchoReport (5300), StructuralHeartReport (5320), GeneralUltrasoundReport (12000)
Radiation Dose ProjectionXRayRadiationDose (10001), CTRadiationDose (10011), RadiopharmaceuticalRadiationDose (10021), PatientRadiationDose (10030), EnhancedXrayRadiationDose (10040)
Imaging Agent PlannedImagingAgentAdministration (11001), PerformedImagingAgentAdministration (11020)
Other ImplantationPlan (7000), PreclinicalAcquisitionContext (8101)

Example:

alias Dicom.SR.{Code, Measurement, MeasurementGroup}
alias Dicom.SR.Templates.MeasurementReport

measurement =
  Measurement.new(
    Code.new("8867-4", "LN", "Heart rate"),
    62,
    Code.new("/min", "UCUM", "beats per minute")
  )

group =
  MeasurementGroup.new("lesion-1", Dicom.UID.generate(),
    measurements: [measurement]
  )

{:ok, document} =
  MeasurementReport.new(
    study_instance_uid: Dicom.UID.generate(),
    series_instance_uid: Dicom.UID.generate(),
    sop_instance_uid: Dicom.UID.generate(),
    observer_name: "REPORTER^ALICE",
    procedure_reported: [Code.new("P5-09051", "SRT", "Chest CT")],
    measurement_groups: [group]
  )

{:ok, data_set} = Dicom.SR.Document.to_data_set(document)
{:ok, binary} = Dicom.write(data_set)

All 33 PS3.16 root templates are implemented. Current scope:

Each builder produces a valid P10-serializable document following the new/1 keyword option pattern.

Quick Start

# Parse a DICOM file
{:ok, data_set} = Dicom.parse_file("/path/to/image.dcm")

# Access attributes by tag
patient_name = Dicom.DataSet.get(data_set, Dicom.Tag.patient_name())
study_date   = Dicom.DataSet.get(data_set, Dicom.Tag.study_date())
modality     = Dicom.DataSet.get(data_set, Dicom.Tag.modality())

# Decode values with VR awareness
raw_element = Dicom.DataSet.get_element(data_set, Dicom.Tag.rows())
rows = Dicom.Value.decode(raw_element.value, raw_element.vr)

# Build a data set from scratch
ds = Dicom.DataSet.new()
    |> Dicom.DataSet.put({0x0002, 0x0002}, :UI, "1.2.840.10008.5.1.4.1.1.2")
    |> Dicom.DataSet.put({0x0002, 0x0003}, :UI, Dicom.UID.generate())
    |> Dicom.DataSet.put({0x0002, 0x0010}, :UI, Dicom.UID.explicit_vr_little_endian())
    |> Dicom.DataSet.put({0x0010, 0x0010}, :PN, "DOE^JOHN")
    |> Dicom.DataSet.put({0x0010, 0x0020}, :LO, "PAT001")

# Serialize and write
{:ok, binary} = Dicom.write(ds)
:ok = Dicom.write_file(ds, "/path/to/output.dcm")

# DataSet bracket access and Enumerable
patient = data_set[Dicom.Tag.patient_name()]
tags = Enum.map(data_set, fn {tag, _elem} -> tag end)

# Tag parsing and date/time conversion
{:ok, tag}  = Dicom.Tag.parse("(0010,0010)")
{:ok, date} = Dicom.Value.to_date("20240115")

# Inspect for quick debugging
IO.inspect(data_set)

Streaming

# Stream events lazily from a file (constant memory)
events = Dicom.stream_parse_file("/path/to/large_image.dcm")

# Filter for specific tags without loading the entire file
patient_tags =
  events
  |> Stream.filter(&match?({:element, %{tag: {0x0010, _}}}, &1))
  |> Enum.map(fn {:element, elem} -> {elem.tag, elem.value} end)

# Or materialize back into a DataSet
{:ok, data_set} =
  Dicom.stream_parse(binary)
  |> Dicom.P10.Stream.to_data_set()

DICOM Standard Coverage

Part Coverage
PS3.4 232 SOP Classes (storage, Q/R, print, worklist) with modality mapping
PS3.5 VR types, transfer syntax handling, sequences, pixel data frame extraction
PS3.6 Tag dictionary (5,035 entries), keyword lookup, retired flags
PS3.10 P10 read/write, File Meta Information, preamble
PS3.15 Best-effort Basic Application Level Confidentiality Profile helpers
PS3.16 Partial SR authoring foundation with focused TID 1500 / 3300 / 3700 builders
PS3.18 DICOM JSON model encoding/decoding for DataSets (Annex F.2)

Transfer syntaxes: Implicit VR LE, Explicit VR LE, Deflated Explicit VR LE, and Explicit VR BE (retired) are fully supported for read and write. Other registered syntaxes (compressed, video) are supported as metadata-only. Unknown UIDs are rejected by default; use TransferSyntax.encoding(uid, lenient: true) to fall back to Explicit VR LE.

Performance

Indicative measurements on Apple Silicon (Elixir 1.18, OTP 27):

Operation Throughput
Parse 50-element data set ~10 µs
Parse 200-element data set ~50 µs
Stream parse 200 elements ~80 µs
Write 50-element data set ~13 µs
Write 200-element data set ~55 µs
Roundtrip 100 elements ~37 µs

Testing

1300+ tests, 16 property-based tests, 35 doctests at 98%+ coverage.

mix test              # 0 failures
mix test --cover      # HTML report in cover/
mix format --check-formatted

Contributing

Contributions are welcome. Please read our Contributing Guide and Code of Conduct before opening a PR.

License

MIT -- see LICENSE for details.