DICOM Network protocol

The DICOM Protocol predates SOAP and REST by quite a lot. One can do four things with it:

  • Test the connection between two devices (C-Echo).

  • Send images from the local imaging device to a remote device (C-Store).

  • Search the content of a remote device (C-Find).

  • Retrieve images from a remote device (C-Move or C-Get).

The client of a DICOM service is known as a service class user (SCU), and the server that handles the requests is called a service class provider (SCP). The client sends a request that is encoded as a DICOM file (the command), and the server answers with a DICOM file.

The connection between a server and client is called an association. An association starts with a handshake where both parties agree on which commands can be exchanged between them and which transfer syntaxes will be supported. The result of this negotiation is called the presentation context. Once the association is negotiated, this communication channel can be used to successively send multiple, independent commands.

Parameters of a DICOM server

A DICOM server can be identified by:

  • Its IP address (or, equivalently, its symbolic DNS hostname).

  • Its TCP port (the standard DICOM port is 104, but Orthanc uses the non-priviliged port 4242 by default).

Along with the above, each imaging device (client or server) must be associated with a symbolic name that is called the application entity title (AET). The AET is assumed to be unique inside the Intranet of the hospital. For best compatibility between vendors, the AET should be only made of alphanumeric characters in upper case (plus the “-” and “_” characters), and its length must be below 16 characters.

So the IP Address, TCP Port and the AET describe all the parameters of a DICOM server.

Commands

  • C-Echo - C-Echo is used to test client-server to test DICOM-level connectivity.

  • C-Store - C-Store is used to send DICOM instances to a remote imaging device

  • C-Find - C-Find is used to search a list of DICOM resources that are hosted by some remote DICOM server. The kind of resource that is needed patients, studies or series) must be specified. The query also contains a set of filters on DICOM tags of interest.

  • C-Move - C-Move is notably used to locally retrieve DICOM files from a remote server, given the results of a C-Find query.

  • C-Get - C-Get provides a simpler alternative to DICOM C-Move, if the issuer and the target correspond to the same modality

Dicom Modalities

Modality is used in radiology to refer to one form of imaging. A DICOM Modality will give information about the type of imaging in the specified DICOM.

Picture archiving and communication system (PACS)

A PACS is a medical imaging technology used to store and access images from multiple modalities. PACS was introduced to essentially eliminate the need to physically and manually file, retrieve, or transport imagery. There. are four components of PACS:

  • Imaging Modalities - This is the image system for doing the actual scanning of a patient in producing a medical image (eg. CT, MRI, PF).
  • A secured network for the transmission of patient information
  • Workstations for interpreting and reviewing images
  • Archives for the storage and retrieval of images and reports

Communication with PACS servers is done through DICOM commands (like the ones listed above). To understand how the communication works, we will be making use of:

  • Orthanc - Orthanc is an open-source DICOM server for medical imaging. It acts as a mini-PACS system. You can get it from here.
  • DCMTK - DCMTK is a collection of libraries and applications implementing large parts the DICOM standard. DCMTK is incredibly expansive, and we will only be using it to run DICOM commands from the terminal.

To run the Orthanc server, I used Docker Compose. Below is what my docker-compose.yml looks like:

version: "3"
services:
  pacs:
    image: jodogne/orthanc-plugins
    ports:
      - 8042:8042
      - 4242:4242
    volumes:
      - ./orthanc.json:/etc/orthanc/orthanc.json:ro
      - orthanc_db:/var/lib/orthanc/db/
    expose: 
      - 4242

volumes: 
  orthanc_db:

The first volume is for the orthanc.json file, the contents of which you can find here (link to json file).

Let’s take each command one by one and use dcmtk to communicate to our orthanc server.

C-ECHO - echoscu

echoscu [options] peer port

echoscu needs 2 arguments to work:

  • peer - hostname of DICOM peer (ip)
  • port - tcp/ip port number of peer

Since you are running the orthanc server locally (you’re technically running it inside the docker container, but we are using port forwarding for port 4242), peer will be 127.0.0.1 (or localhost or 0.0.0.0). For the port, you will have to look at the config JSON file that is used with your Orthanc server and look for DicomPort. The default value is 4242.

Ensure that the Orthanc server is running, and we can run the command

echoscu localhost 4242

	$ echoscu localhost 4242 
	$ 

There was no output, let’s add the -v flag and see what happens

$ echoscu -v localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Echo Request (MsgID 1)
I: Received Echo Response (Success)
I: Releasing Association

That’s a good start! We’re able to communicate to our orthanc server without issues. Let’s move onto actually transferring data now.

C-STORE - storescu

Sending a DICOM to the orthanc server

storescu [options] peer port dcmfile-in

dcmfile-in is the path of the dicom file you want to send. Let’s try this out.

$ storescu localhost 4242 demo1.dcm
W: DIMSE Warning: (STORESCU,ANY-SCP): sendMessage: unable to convert dataset from 'JPEG Lossless, Non-hierarchical, 1st Order Prediction' transfer syntax to 'Little Endian Explicit'
E: Store Failed, file: demo1.dcm:
E: 0006:020e DIMSE Failed to send message
E: Store SCU Failed: 0006:020e DIMSE Failed to send message
$ 

Well, we get an error saying it cannot dataset from ‘JPEG Lossless, Non-hierarchical, 1st Order Prediction’ transfer syntax to ‘Little Endian Explicit’. To fix this, let’s add the -xs flag. From the docs, -xsis used to propose default JPEG lossless TS and all uncompressed transfer syntaxes. Let’s try again.

$ storescu -xs localhost 4242 demo1.dcm
$ 

No errors now! Let’s check Orthanc again

There it is, we have used C-STORE to send a dicom file to our Orthanc server! You can use -v here too.

$ storescu -xs -v localhost 4242 demo1.dcm
I: checking input files ...
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending file: demo1.dcm
I: Converting transfer syntax: JPEG Lossless, Non-hierarchical, 1st Order Prediction -> JPEG Lossless, Non-hierarchical, 1st Order Prediction
I: Sending Store Request (MsgID 1, CR)
XMIT: ..................................................................................................................................................................................................
I: Received Store Response (Success)
I: Releasing Association

C-FIND - findscu

Sending a query to find DICOMs

This is where things get interesting. Look art this findscu example from the dcmtk FAQs

findscu -v -P -k 0008,0052="IMAGE" -k 0010,0020="300019" -k 0020,000D="1.2.3.1" -k 0020,000E="1.2.3.2" -k 0008,0018="1.2.3.3" localhost 104

To begin with, we will use hex values for the DICOM tags!

-P is to use patient root information model -k is to look for values according to the dicom tags. 0010,0020 is the patient ID for example. (0010,0010 is patient name). You can also use dictionary names instead of the hex values

With this information, let’s try this ourselves. Below is the study I’ll try to find.

Let’s run this simple command: findscu -v -P -k "(0008,0052)=PATIENT" -k PatientID="CLU121161" localhost 4242

$ findscu -v -P -k "(0008,0052)=PATIENT" -k PatientID="CLU121161" localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Find Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0052) CS [PATIENT]                                #   8, 1 QueryRetrieveLevel
I: (0010,0020) LO [CLU121161]                              #  10, 1 PatientID
I: 
E: Find Failed, query keys:
E: 
E: # Dicom-File-Format
E: 
E: # Dicom-Meta-Information-Header
E: # Used TransferSyntax: Little Endian Explicit
E: 
E: # Dicom-Data-Set
E: # Used TransferSyntax: Little Endian Explicit
E: (0008,0052) CS [PATIENT ]                               #   8, 1 QueryRetrieveLevel
E: (0010,0020) LO [CLU121161 ]                             #  10, 1 PatientID
E: 
E: 0006:0317 Peer aborted Association (or never connected)
I: Peer Aborted Association

Hmm. It says peer aborted association? Let’s look at what the Orthanc server is saying.

Rejected Find request from remote DICOM modality with AET "FINDSCU" and hostname "172.29.0.1"

It’s rejecting our find request. This is because it needs the dicom modality info in its config file. In "DicomModalities", based on my IP that it receives (it’s not local because I’m running orthanc in a docker container), I am adding: "test" : ["FINDSCU", "172.29.0.1", 2000],

It now knows to accept requests from AETitle FINDSCU (which is the default title used when findscu is used) with the given IP address and port. Let’s restart the server and try again.

$ findscu -v -P -k "(0008,0052)=PATIENT" -k PatientID="CLU121161" localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Find Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0052) CS [PATIENT]                                #   8, 1 QueryRetrieveLevel
I: (0010,0020) LO [CLU121161]                              #  10, 1 PatientID
I: 
I: ---------------------------
I: Find Response: 1 (Pending)
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS [ISO_IR 100]                             #  10, 1 SpecificCharacterSet
I: (0008,0052) CS [PATIENT ]                               #   8, 1 QueryRetrieveLevel
I: (0008,0054) AE [ORTHANC ]                               #   8, 1 RetrieveAETitle
I: (0010,0020) LO [CLU121161 ]                             #  10, 1 PatientID
I: 
I: Received Final Find Response (Success)
I: Releasing Association

It works! 0008,0052 is the Query/Retrieve Level and needs to be specified each time. There are 4 possibilities (PATIENT, STUDY, SERIES or IMAGE) and it is used to target what level we would like to query at. In the example above, I am querying at patient level, looking for PatientID="CLU121161. While my query was a success, the information retrieved is not of much help. Let’s change this.

$ findscu -P -k "(0008,0052)=PATIENT" -k PatientID="CLU121161" -k PatientName localhost 4242 
I: ---------------------------
I: Find Response: 1 (Pending)
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS [ISO_IR 100]                             #  10, 1 SpecificCharacterSet
I: (0008,0052) CS [PATIENT ]                               #   8, 1 QueryRetrieveLevel
I: (0008,0054) AE [ORTHANC ]                               #   8, 1 RetrieveAETitle
I: (0010,0010) PN [ABCD]                                   #   4, 1 PatientName
I: (0010,0020) LO [CLU121161 ]                             #  10, 1 PatientID

This is already better! I added -k PatientName, to find out what the Name of the patient with PatientID="CLU121161" is. Let’s take it a step further:

$ findscu -v -P -k "(0008,0052)=PATIENT" -k PatientID -k PatientName="A*" localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Find Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0052) CS [PATIENT]                                #   8, 1 QueryRetrieveLevel
I: (0010,0010) PN [A*]                                     #   2, 1 PatientName
I: (0010,0020) LO (no value available)                     #   0, 0 PatientID
I: 
I: ---------------------------
I: Find Response: 1 (Pending)
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS [ISO_IR 100]                             #  10, 1 SpecificCharacterSet
I: (0008,0052) CS [PATIENT ]                               #   8, 1 QueryRetrieveLevel
I: (0008,0054) AE [ORTHANC ]                               #   8, 1 RetrieveAETitle
I: (0010,0010) PN [ABCD]                                   #   4, 1 PatientName
I: (0010,0020) LO [CLU121161 ]                             #  10, 1 PatientID
I: 
I: Received Final Find Response (Success)
I: Releasing Association

This is sweet! If you didn’t follow the command, I’m looking for patients with names starting with A, and getting the PatientID in return! Here’s another example

$ findscu -v -P -k 0008,0052="IMAGE" -k 0010,0020="CLU121161" -k 0020,000D="1.2.840.10008.1.500817.567467863.1560288934.406684597" -k 0020,000E="1.2.392.200036.9125.3.331555024156.64663542911.19447643" -k 0008,0018="1.2.392.200036.9125.9.0.235875605.654909696.849058840" -k Modality -k PerformedProcedureStepDescription -k PatientName -k PatientID localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Find Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0018) UI [1.2.392.200036.9125.9.0.235875605.654909696.849058840] #  54, 1 SOPInstanceUID
I: (0008,0052) CS [IMAGE]                                  #   6, 1 QueryRetrieveLevel
I: (0008,0060) CS (no value available)                     #   0, 0 Modality
I: (0010,0010) PN (no value available)                     #   0, 0 PatientName
I: (0010,0020) LO (no value available)                     #   0, 0 PatientID
I: (0020,000d) UI [1.2.840.10008.1.500817.567467863.1560288934.406684597] #  54, 1 StudyInstanceUID
I: (0020,000e) UI [1.2.392.200036.9125.3.331555024156.64663542911.19447643] #  56, 1 SeriesInstanceUID
I: (0040,0254) LO (no value available)                     #   0, 0 PerformedProcedureStepDescription
I: 
I: ---------------------------
I: Find Response: 1 (Pending)
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS [ISO_IR 100]                             #  10, 1 SpecificCharacterSet
I: (0008,0018) UI [1.2.392.200036.9125.9.0.235875605.654909696.849058840] #  54, 1 SOPInstanceUID
I: (0008,0052) CS [IMAGE ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [ORTHANC ]                               #   8, 1 RetrieveAETitle
I: (0008,0060) CS [CR]                                     #   2, 1 Modality
I: (0010,0010) PN [ABCD]                                   #   4, 1 PatientName
I: (0010,0020) LO [CLU121161 ]                             #  10, 1 PatientID
I: (0020,000d) UI [1.2.840.10008.1.500817.567467863.1560288934.406684597] #  54, 1 StudyInstanceUID
I: (0020,000e) UI [1.2.392.200036.9125.3.331555024156.64663542911.19447643] #  56, 1 SeriesInstanceUID
I: (0040,0254) LO [X-RAY CHEST PA]                         #  14, 1 PerformedProcedureStepDescription
I: 
I: Received Final Find Response (Success)
I: Releasing Association

I’m getting the Modality, Procedure Description, PatientName using this query!

Before moving ahead, I’m adding these to my orthanc config’s DicomModalities:

"test2" : ["MOVESCU", "172.29.0.1", 2000],
"test3" : ["GETSCU", "172.29.0.1", 2000]

You could alternatively only have one entry, and use that AE Title in every command using the -aet flag

C-Move - movescu

The way C-Move works is slightly different. It involves three parties instead of two. The first is the one issuing the C-Move command, the second is the server where the dicom file to be retrieved exists and the third is the destination AE. The issuer and destination can be the same.

To get started with C-Move, we’ll have to change our docker setup a bit. Let us edit our docker file to have two orthanc instances running instead.

This is what my new docker-compose.yml file looks like:

version: "3"
services:
  pacs:
    image: jodogne/orthanc-plugins
    ports:
      - 8042:8042
      - 4242:4242
    volumes:
      - ./orthanc.json:/etc/orthanc/orthanc.json:ro
      - orthanc_db:/var/lib/orthanc/db/
    expose: 
      - 4242

  pacs2:
    image: jodogne/orthanc-plugins
    ports:
      - 8043:8043
      - 4243:4243
    volumes:
      - ./orthanc2.json:/etc/orthanc/orthanc.json:ro
      - orthanc_db2:/var/lib/orthanc/db2/
    expose:
      - 4243

volumes: 
  orthanc_db:

  orthanc_db2:

A different config file is used for the second Orthanc instance so that we can configure it with a different AE Title, Dicom Port and HTTP Port (4242, 8042 and 4243, 8043 respectively). Here is the second config file if you want to take a look. We also have to edit the configs to enter new entries in DicomModalities, so that both Orthanc servers know of each other. Here is a link to the first config file just in case.

With this, let’s run docker-compose up and you should be able to access both Orthanc servers at http://localhost:8042 and http://localhost:8043.

Let’s try using movescu now. Our goal is to issue a movescu command to look for and send a dicom file from the first Orthanc server to the second Orthanc Server.

(base) milind@Milinds-MacBook-Air ohif-docker-compose % movescu -v -P -k 0008,0052="PATIENT" -k PatientID="CLU121161" -aem "ORTHANC2" localhost 4242

I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Move Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0052) CS [PATIENT]                                #   8, 1 QueryRetrieveLevel
I: (0010,0020) LO [CLU121161]                              #  10, 1 PatientID
I: 
I: Received Final Move Response (Success)
I: Releasing Association

It says success, let’s look at our second Orthanc server again

It worked! If you look at the command again, -aem "ORTHANC2" is used to tell the SCU to send the dicom to AE Title ORTHANC2 from its records. If you look back at the config file, you will see that I have added an entry for the same so that the server knows all info about ORTHANC2.

The more challenging bit is to get your Orthanc server to send a dicom to the host machine’s movescu. To get this working, add the following entry to your Orthanc server’s DicomModalities:

"store" : ["STORESCP", "host.docker.internal", 2001]

This is basically letting the Orthanc server know of AE Title STORESCP with the address host.docker.internal and port 2001. This is because we’ll be running the storescp command that will listen on port 2001. host.docker.internal is used to communicate with the host machine. Let’s try it out now!

We’ll first start with storescp by using the command storescp -v 2001 (-v is for verbose mode). After this we will use movescu by using movescu -v -P -k 0008,0052="PATIENT" -k PatientID="CLU121161" -aem "STORESCP" localhost 4242

While the other flags might look familiar from above, -aem "STORESCP" is new. It lets the Orthanc server know to send the dicom to AE Title STORESCP (the info of which it should already have, since we added it to the config).

storescp

(base) milind@Milinds-MacBook-Air ohif-docker-compose % storescp -v 2001
I: Association Received
I: Association Acknowledged (Max Send PDV: 16372)
I: Received Store Request (MsgID 1, CR)
RECV: .............................................................................................................................................................................................................................................................................................................................................................................................................................................................................
I: storing DICOM file: ./CR.1.2.392.200036.9125.9.0.235875605.654909696.849058840
I: Association Release

movescu

(base) milind@Milinds-MacBook-Air ohif-docker-compose % movescu -v -P -k 0008,0052="PATIENT" -k PatientID="CLU121161" -aem "STORESCP" localhost 4242
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending Move Request (MsgID 1)
I: Request Identifiers:
I: 
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0052) CS [PATIENT]                                #   8, 1 QueryRetrieveLevel
I: (0010,0020) LO [CLU121161]                              #  10, 1 PatientID
I: 
I: Received Final Move Response (Success)
I: Releasing Association

C-GET - getscu

Used to retrieve DICOMs

Let’s get ourselves familiar with dcmtk’s dump2dcm here first. You can use it to create dcm files (to be used for querying) with certain DICOM tag values. For Example:

```
$ cat test
# contents of file test
# request all images for the patient with ID=CLU121161

(0008,0052) CS [PATIENT]     # QueryRetrieveLevel
(0010,0020) LO [CLU121161]      # PatientID%      

$ dump2dcm test query.dcm
W: output transfer syntax unknown, assuming --write-xfer-little

$ ls
demo1.dcm		demo2.dcm		ohif-docker-compose	query.dcm		test
```

Let’s use the query.dcm file now.

Command: getscu [options] peer port [dcmfile-in...]

This one seems, straightforward, let’s try it.

$ ls
demo1.dcm		demo2.dcm		ohif-docker-compose	query.dcm		test
$ getscu -v localhost 4242 query.dcm

I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending C-GET Request (MsgID 1)
I: Received C-STORE Request (MsgID 1)
I: Sending C-STORE Response (Success)
I: Received C-GET Response (Success)
I: Final status report from last C-GET message:
I:   Number of Remaining Suboperations : 0
I:   Number of Completed Suboperations : 1
I:   Number of Failed Suboperations    : 0
I:   Number of Warning Suboperations   : 0
I: Releasing Association
$ ls
CR.1.2.392.200036.9125.9.0.235875605.654909696.849058840	demo2.dcm							query.dcm
demo1.dcm							ohif-docker-compose						test

The query file is being used (-k can be used too) here to look for a dicom and retrieve it.