Dicom.ex is a Elixir library implementing the DICOM standard for data storage and network transfers.
Attention: This project is a personal project to learn Elixir and DICOM internals. It should not be used in production and absolutely not in clinical contexts.
- General methods to work with DICOM data sets and elements
- Read data sets from files encode according to DICOM Part 3.10
- Supports VRs and tag dictionary as of DICOM version 2024d
- Receive C-ECHO, C-FIND and C-STORE network requests (DICOM Part 3.7)
At the moment of configuring SCP services, the final developer must define functions called event_handlers
which will take
care of the response to the SCU, based on the data received.
At the moment of writing this readme, there are these handlers:
association_validator
Allows/Rejects incomming connections.
cfind
Receives incoming C-FIND requests and returns a Stream containing matches.
cstore
Receives incoming C-STORE requests. The handler have access to the incoming dicom file and must take care of saving to filesystem, database, etc.
The following examples shows how to parse and access/write data from/to dicom files.
ds =
Dicom.DataSet.from_keyword_list(
SOPInstanceUID: "1.2.3",
PatientID: "ABC123",
ImageType: ["Test1", "Test2", "Test3"]
)
assert Dicom.DataSet.value_for!(ds, :PatientID) == "ABC123"
assert Dicom.DataSet.value_for!(ds, :ImageType, 2) == "Test3"
ds = Dicom.BinaryFormat.from_file!("test/test_files/test-ExplicitVRLittleEndian.dcm")
The following examples show how to create Dicom SCP services and their handlers.
This example allows incoming requests pointing to TEST
AETitle, all other AETitles are rejected.
To test an accepting association you could run echoscu (from dcmtk) this way:
echoscu -d -aec TEST 127.0.0.1 4242
To test an rejecting association:
echoscu -d -aec OTHERAET 127.0.0.1 4242
defmodule AssociationExample do
use Application
def start(_type, _args) do
{:ok, endpoint_pid} = GenServer.start_link(
DicomNet.Endpoint,
port: 4242,
event_handlers: [
association_validator: &association_handler/1,
]
)
loop()
{:ok, endpoint_pid}
end
# Whithout this the server won't keep running
defp loop() do
loop()
end
defp association_handler(association_data) do
case association_data.called_ae_title do
"TEST" ->
:accept
_ ->
{:reject, :dicom_ul_service_user, :called_ae_title_not_recognized}
end
end
end
Adding up to the association validation example, we'll add a cfind
handler:
defmodule CFindSCP do
use Application
def start(_type, _args) do
{:ok, endpoint_pid} = GenServer.start_link(
DicomNet.Endpoint,
port: 4242,
event_handlers: [
cfind: &get_responses/1,
association_validator: &association_handler/1,
]
)
loop()
{:ok, endpoint_pid}
end
# Whithout this the server won't keep running
defp loop() do
loop()
end
defp association_handler(association_data) do
case association_data.called_ae_title do
"TEST" ->
:accept
_ ->
{:reject, :dicom_ul_service_user, :called_ae_title_not_recognized}
end
end
defp get_responses(dataset) do
# Rename tags with atoms
fields = Enum.map(dataset, fn {k, v} ->
group = v.group_number
element = v.element_number
values = v.values
case {group, element} do
{0x0008, 0x0050} -> {:AccessionNumber, values}
{0x0010, 0x0010} -> {:PatientName, values}
{0x0010, 0x0020} -> {:PatientID, values}
_ -> {:Ignore, nil}
end
end
)
# sample study list
studies = [
["PatientName": "Carmack^John", PatientID: "1", AccessionNumber: "A001"],
["PatientName": "Kernighan^Brian", PatientID: "2", AccessionNumber: "A002"],
["PatientName": "Torvalds^Linus", PatientID: "3", AccessionNumber: "A003"],
["PatientName": "Van Rossum^Guido", PatientID: "4", AccessionNumber: "A004"],
["PatientName": "Valim^José", PatientID: "5", AccessionNumber: "A005"]
]
# Return the list excluding those fields not present in fields list.
Stream.map(studies, fn study ->
Keyword.take(study, Keyword.keys(fields))
|>Dicom.DataSet.from_keyword_list()
end)
end
end
This last example includes all the handlers available at this moment, association_validator
, cfind
and cstore
.
defmodule SCP do
use Application
def start(_type, _args) do
{:ok, endpoint_pid} = GenServer.start_link(
DicomNet.Endpoint,
port: 4242,
event_handlers: [
cfind: &get_responses/1,
cstore: &store_image/1,
association_validator: &association_handler/1,
]
)
loop()
{:ok, endpoint_pid}
end
# Whithout this the server won't keep running
defp loop() do
loop()
end
defp association_handler(association_data) do
case association_data.called_ae_title do
"TEST" ->
:accept
_ ->
{:reject, :dicom_ul_service_user, :called_ae_title_not_recognized}
end
end
defp get_responses(dataset) do
# Rename tags with atoms
fields = Enum.map(dataset, fn {k, v} ->
group = v.group_number
element = v.element_number
values = v.values
case {group, element} do
{0x0008, 0x0050} -> {:AccessionNumber, values}
{0x0010, 0x0010} -> {:PatientName, values}
{0x0010, 0x0020} -> {:PatientID, values}
_ -> {:Ignore, nil}
end
end
)
# sample study list
studies = [
["PatientName": "Carmack^John", PatientID: "1", AccessionNumber: "A001"],
["PatientName": "Kernighan^Brian", PatientID: "2", AccessionNumber: "A002"],
["PatientName": "Torvalds^Linus", PatientID: "3", AccessionNumber: "A003"],
["PatientName": "Van Rossum^Guido", PatientID: "4", AccessionNumber: "A004"],
["PatientName": "Valim^José", PatientID: "5", AccessionNumber: "A005"]
]
# Return the list excluding those fields not present in fields list.
Stream.map(studies, fn study ->
Keyword.take(study, Keyword.keys(fields))
|>Dicom.DataSet.from_keyword_list()
end)
end
defp store_image(data) do
# just print the incoming dataset
IO.inspect(data, label: "C-STORE")
end
end