Skip to content

jjedele/dicom.ex

Repository files navigation

Dicom.ex

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.

Features

  • 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)

SCP Handlers

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.

Examples

Dicom parsing and writing

The following examples shows how to parse and access/write data from/to dicom files.

Create and access DICOM data set

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"

Read DICOM data set from file

ds = Dicom.BinaryFormat.from_file!("test/test_files/test-ExplicitVRLittleEndian.dcm")

Dicom SCP

The following examples show how to create Dicom SCP services and their handlers.

Association Validation

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

C-FIND SCP

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

C-STORE SCP

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