Current section
Files
Jump to
Current section
Files
README.md
# Dicom[](https://hex.pm/packages/dicom)[](https://hexdocs.pm/dicom)[](https://github.com/Balneario-de-Cofrentes/dicom/actions)[](https://opensource.org/licenses/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- **P10 file parsing** -- read DICOM Part 10 files into structured data sets- **P10 file writing** -- serialize data sets back to Part 10 binaries with File Meta Information- **Streaming parser** -- lazy, event-based parsing for large files and pipelines (stable API)- **Data dictionary** -- PS3.6 tag registry (5,035 entries) with VR, VM, keyword lookup, and retired flags- **DICOM JSON** -- encode/decode DataSets to/from the DICOM JSON model (PS3.18 Annex F.2) for DICOMweb- **Pixel data frames** -- extract individual frames from native and encapsulated pixel data (PS3.5 §A.4)- **De-identification** -- PS3.15 Basic Profile helpers with consistent UID replacement and private tag control- **Structured Reports** -- reusable PS3.16 building blocks plus focused builders for TID 1500, TID 3300, and TID 3700- **Character set support** -- single-byte Specific Character Set repertoires and UTF-8- **SOP Class registry** -- 232 SOP Classes with modality mapping and O(1) lookup- **Transfer syntaxes** -- 49 tracked (34 active + 15 retired); unknown UIDs rejected by default- **Sequences** -- defined-length and undefined-length SQ with nested items- **Zero dependencies** -- pure Elixir, no NIFs, no external tools## InstallationAdd `dicom` to your `mix.exs` dependencies:```elixirdef deps do [ {:dicom, "~> 0.9.0"} ]end```## Structured Reports`dicom` includes complete PS3.16 SR authoring with all 33 root template builders:- coded entries via `Dicom.SR.Code` (372 normative codes)- content-tree construction via `Dicom.SR.ContentItem` (13 value types incl. TCOORD)- observation context helpers via `Dicom.SR.Observer`- SR document rendering via `Dicom.SR.Document`- **34 template builders** covering every PS3.16 root template:| 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:```elixiralias Dicom.SR.{Code, Measurement, MeasurementGroup}alias Dicom.SR.Templates.MeasurementReportmeasurement = 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:- implemented: all root template builders, 372 normative codes, TCOORD value type, shared helpers- not yet implemented: full CID validation, exhaustive sub-template hierarchies, complete spatial/segmentation linkageEach builder produces a valid P10-serializable document following the `new/1` keyword option pattern.## Quick Start```elixir# Parse a DICOM file{:ok, data_set} = Dicom.parse_file("/path/to/image.dcm")# Access attributes by tagpatient_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 awarenessraw_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 scratchds = 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 Enumerablepatient = 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 debuggingIO.inspect(data_set)```### Streaming```elixir# 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 filepatient_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.## PerformanceIndicative 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 |## Testing1300+ tests, 16 property-based tests, 35 doctests at 98%+ coverage.```bashmix test # 0 failuresmix test --cover # HTML report in cover/mix format --check-formatted```## ContributingContributions are welcome. Please read our [Contributing Guide](CONTRIBUTING.md)and [Code of Conduct](CODE_OF_CONDUCT.md) before opening a PR.## LicenseMIT -- see [LICENSE](LICENSE) for details.