Project

General

Profile

PACS Debugging with DCMTK

Introduction

DCMTK is not only a set of libraries but also includes a couple of tools that demonstrate the capabilities of the libraries. Some of them are very handy in order to find problems in DICOM files, or to simulate and debug DICOM network nodes.

This howto shows tools one can use to debug PACS communication by the help of DCMTK's Storage and Query/Retrieve tools on client and server side. The tools used will be echoscu, storescu, findscu and movescu on the client side, as well as dcmqrscp (and again movescu, as we will see) on the server side. DICOM' Storage and Query/Retrieve protocol is not described in detail here.

What is a PACS at all? There is not even a definition in DICOM. DICOM only describes network services by defining SOP Classes. There are SOP Classes for image (or more general: object) transfer, for explicit archiving, for printing and so on. However, there are a couple of SOP classes that usually everyone associates with a "PACS":

  • Verification SOP Class ("DICOM ECHO"): Find out whether the system on the other side (e.g. a "PACS") actually "talks DICOM" on the network
  • One or more Storage SOP Classes (e.g. "CT Image Storage SOP Class"): Store one ore more DICOM objects (e.g. CT images) to the PACS. In the rest of the text the term "image" instead of objects is used to be more readable.
  • One or more Query SOP Classes (e.g. "Study Root Query/Retrieve Information Model - FIND"): Search PACS database using pre-defined search keys. Does not download any images at all, but only information about the images.
  • One or more Retrieve SOP Classes (e.g. Study Root Query/Retrieve Information Model - MOVE"): Once you have identified the images you'd like to download, you can retrieve them with one of the Retrieve SOP Classes.
  • Storage Commitment SOP Classes (e.g. Storage Commitment Push Model SOP Instance): Used by clients to ensure that server has archived (as opposed of just having received them) a couple of images.

In this howto we make use of the above SOP classes except the Storage Commitment part. The reason is that there are no Storage Commitment tools inside the public Open Source DCMTK, though you can buy Storage Commitment based on DCMTK from OFFIS if you like.

Prerequisites

This Howto uses some files (image files and a PACS configuration file) which are attached to this page (you can find it at the end of this page). All tools have been used on a Linux system; output may look slightly different on Windows but the tools work the same way on other Operating Systems.

This howto explains some DICOM details of what is going on. However, it makes sense to get a basic understanding of DICOM networking first. As a primer this DICOM Networking tutorial may be helpful. Then read the Howto about understanding DCMTK's association negotiation logging output which helps you to understand the log files shown in this tutorial and also helps with identifying problems.

Setting up dcmqrscp

dcmqrscp is a tool that implements various SOP Classes that are normally expected from a PACS:
  • Verification SOP Class
  • Various Storage SOP Classes
  • Query SOP Classes:
    • Patient Root Query/Retrieve Information Model - FIND
    • Study Root Query/Retrieve Information Model - FIND
    • Patient/Study Only Query/Retrieve Information Model - FIND (retired in DICOM)
  • Retrieve SOP Classes:
    • Patient Root Query/Retrieve Information Model - GET
    • Study Root Query/Retrieve Information Model - GET
    • Patient/Study Only Query/Retrieve Information Model - GET (retired in DICOM)
    • Patient Root Query/Retrieve Information Model - MOVE
    • Study Root Query/Retrieve Information Model - MOVE
    • Patient/Study Only Query/Retrieve Information Model - MOVE (retired in DICOM)

Like all of the DCMTK tools, a short help page is provided by using the "--help" option ("dcmqrscp --help"). Lots of options to scroll through. They are also listed, together with additional information, whithin the tool's manual that comes with DCMTK and is also available online (see Introduction section for the links). dcmqrscp cannot be configured solely by command line options. Instead it requires a mandatory configuration file. A default file comes with DCMTK and is called dcmqrscp.cfg. We will adapt it for our tests. For a complete copy of the modified file check the attached ZIP. If you want to apply the changes discussed below for yourself, locate the original default file that comes with DCMTK and copy it to your working directory, i.e. the directory where your command prompt currently is. Open it in a text editor and you see some options that are read by dcmqrscp on startup. For now, you only need to edit some of them. All lines starting with "#" are comments (you may add your own). The first real entries are:

NetworkTCPPort  = 104
MaxPDUSize      = 16384
MaxAssociations = 16

You should change the Network TCP Port to the port where you want dcmqrscp to listen for incoming connections. On Unix systems, choose something >= 1024 in order to avoid privilege problems. Make sure your firewall is not blocking access to this port (on Windows, often a Window pops up once you start dcmqrscp -- allow dcmqrscp to open the selected port). Let's select 11112 for our testing.

The next table is used to define the machines that will access dcmqrscp over the network. This is the default:

acme1           = (ACME1, acmehost1, 5678)
acme2           = (ACME2, acmehost2, 5678)
acmeCTcompany   = acme1, acme2
united1         = (UNITED1, unitedhost1, 104)
united2         = (UNITED2, unitedhost2, 104)
unitedMRcompany = united1, united2

The format is described within the configuration file comments (not shown here). Just note that we define "acmeCTcompany" to include two AE Titles: ACME1 and ACME2, on different hosts, and both listening on port 5678 for incoming storage connections. The port is needed if we want dcmqrscp later on to send images to these systems (Retrieve via "C-MOVE"). We'll dig into this later. The same happens for unitedMRCompany. You should at least exchange the host name "acmehost1", etc. with the host name of your own system. If you only will connect to dcmqrscp from clients that are on the same host, you will enter "localhost" for each. You can remove any lines that you do not need, or leave them inside (they won't hurt). For the rest of the tutorial, we assume we test everything on a single host, so you have entered "localhost". This is the configuration we assume working with:

acme1           = (ACME1, localhost, 1234)
acme2           = (ACME2, localhost, 5678)
acmeCTcompany   = acme1, acme2

Another table follows which assigns more human-readable names to the symbolic names acmeCTcompany and unitedMRcompany:

"Acme CT Company"   = acmeCTcompany
"United MR Company" = unitedMRcompany

You can leave it as is, or change the strings within quotes to something you like. Now, at the end of the configuration file, an important table follows where you need to edit something. dcmqrscp can assign different storage areas to each of the symbolic company names, i.e. it can restrict the disk space that every "company" may use, the number of studies, and whether it can read or only write to it. Additionally, each of those storage areas goes into a different directory. This is the default value:

COMMON       /home/dicom/db/COMMON       R  (200, 1024mb) ANY
ACME_STORE   /home/dicom/db/ACME_STORE   RW (9, 1024mb)   acmeCTcompany
UNITED_STORE /home/dicom/db/UNITED_STORE RW (9, 1024mb)   unitedMRcompany

We change it to:

ACME_STORE   /tmp/database   RW (100, 1024mb)   acmeCTcompany

This means that the storage area should be located in the directory "/tmp/database". You can select any existing folder that you (your user account) is permitted to write to. So for our testing example, you must create "/tmp/database" before starting dcmqrscp. On Windows, use the normal drive notation for the path, e.g. "c:\your\folder". "RW" means acmeCTcompany is permitted to read and write this storage area. A maximum of 100 studies will be permitted, with 1024 MB maximum overall storage size.

Here is for reference the final configuration file that we put together (comments removed):

NetworkTCPPort  = 11112
MaxPDUSize      = 16384
MaxAssociations = 16

HostTable BEGIN
acme1           = (ACME1, localhost, 1234)
acme2           = (ACME2, localhost, 5678)
acmeCTcompany   = acme1, acme2
HostTable END

VendorTable BEGIN
"Acme CT Company"   = acmeCTcompany
VendorTable END

AETable BEGIN
ACME_STORE   /tmp/database   RW (100, 1024mb)   acmeCTcompany
AETable END

Now, we can actually start dcmqrscp. We do not need any options for now but must point it to the directory where the configuration file is (which you copied to the current directory). The final call is:

dcmqrscp --config dcmqrscp.cfg

Let's start sending messages to the server!

Testing the PACS Connection with echoscu

Before you start here, you should have a PACS available to test with. I assume you have setup dcmqrscp as described above. In order to test the connection to any DICOM server, we can use the Verification SOP class which must be supported by every SCP. We just need to provide the host and port, and enable debug mode on top (-d) to see some output:

michael@einstein:~$ echoscu -d localhost 11112
D: $dcmtk: echoscu v3.6.1 DEV $
D: 
D: DcmDataDictionary: Loading file: /usr/local/share/dcmtk/dicom.dic
D: Request Parameters:
D: ====================== BEGIN A-ASSOCIATE-RQ =====================
D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
D: Our Implementation Version Name:   OFFIS_DCMTK_361
D: Their Implementation Class UID:    
D: Their Implementation Version Name: 
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    ECHOSCU
D: Called Application Name:     ANY-SCP
D: Responding Application Name: resp. AP Title
D: Our Max PDU Receive Size:    16384
D: Their Max PDU Receive Size:  0
D: Presentation Contexts:
D:   Context ID:        1 (Proposed)
D:     Abstract Syntax: =VerificationSOPClass
D:     Proposed SCP/SCU Role: Default
D:     Proposed Transfer Syntax(es):
D:       =LittleEndianImplicit
D: Requested Extended Negotiation: none
D: Accepted Extended Negotiation:  none
D: Requested User Identity Negotiation: none
D: User Identity Negotiation Response:  none
D: ======================= END A-ASSOCIATE-RQ ======================
I: Requesting Association
D: setting network send timeout to 60 seconds
D: setting network receive timeout to 60 seconds
D: Constructing Associate RQ PDU
F: Association Rejected:
F: Result: Rejected Permanent, Source: Service User
F: Reason: Called AE Title Not Recognized
michael@einstein:~$

Oops, there are some lines that start with "F:" which is the fatal level log level, so something went really wrong. What can we see from the logs? There seems to be a server, otherwise we would have run into a timeout. However, we receive a rejection of our request ("Association Rejected"). And the server says, the error will not go away if we re-try with the same parameters later ("Rejected Permanent"). The good thing is that the server tells us the reason why the Verification fails: "Called AE Title Not Recognized". Ah, remember? We had setup some storage areas in dcmqrscp's configuration file. Those storage areas are addressed by using the name of the area as "Called AE Title". All Called AE Titles not matching a storage area name will be rejected by the server! So let's re-try the ECHO by using echoscu's "-aec" option to specify the "Called AE Title":

michael@einstein:~$ echoscu -d localhost 11112 -aec ACME_STORE
D: $dcmtk: echoscu v3.6.1 DEV $
D: 
D: DcmDataDictionary: Loading file: /usr/local/share/dcmtk/dicom.dic
D: Request Parameters:
D: ====================== BEGIN A-ASSOCIATE-RQ =====================
D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
D: Our Implementation Version Name:   OFFIS_DCMTK_361
D: Their Implementation Class UID:    
D: Their Implementation Version Name: 
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    ECHOSCU
D: Called Application Name:     ACME_STORE
D: Responding Application Name: resp. AP Title
D: Our Max PDU Receive Size:    16384
D: Their Max PDU Receive Size:  0
D: Presentation Contexts:
D:   Context ID:        1 (Proposed)
D:     Abstract Syntax: =VerificationSOPClass
D:     Proposed SCP/SCU Role: Default
D:     Proposed Transfer Syntax(es):
D:       =LittleEndianImplicit
D: Requested Extended Negotiation: none
D: Accepted Extended Negotiation:  none
D: Requested User Identity Negotiation: none
D: User Identity Negotiation Response:  none
D: ======================= END A-ASSOCIATE-RQ ======================
I: Requesting Association
D: setting network send timeout to 60 seconds
D: setting network receive timeout to 60 seconds
D: Constructing Associate RQ PDU
F: Association Rejected:
F: Result: Rejected Permanent, Source: Service User
F: Reason: Called AE Title Not Recognized

Hm, not yet correct. Still the same error. Ok, this is a problem with dcmqrscp, but I know actually the server requires also the right "Calling AE Title" to be used, and we setup ACME1 and ACME2, right? So let's retry and also set our own AE Title correctly!

michael@einstein:~$ echoscu -d localhost 11112 -aec ACME_STORE -aet ACME1    
D: $dcmtk: echoscu v3.6.1 DEV $
D: 
D: DcmDataDictionary: Loading file: /usr/local/share/dcmtk/dicom.dic
D: Request Parameters:
D: ====================== BEGIN A-ASSOCIATE-RQ =====================
D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
D: Our Implementation Version Name:   OFFIS_DCMTK_361
D: Their Implementation Class UID:    
D: Their Implementation Version Name: 
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    ACME1
D: Called Application Name:     ACME_STORE
D: Responding Application Name: resp. AP Title
D: Our Max PDU Receive Size:    16384
D: Their Max PDU Receive Size:  0
D: Presentation Contexts:
D:   Context ID:        1 (Proposed)
D:     Abstract Syntax: =VerificationSOPClass
D:     Proposed SCP/SCU Role: Default
D:     Proposed Transfer Syntax(es):
D:       =LittleEndianImplicit
D: Requested Extended Negotiation: none
D: Accepted Extended Negotiation:  none
D: Requested User Identity Negotiation: none
D: User Identity Negotiation Response:  none
D: ======================= END A-ASSOCIATE-RQ ======================
I: Requesting Association
D: setting network send timeout to 60 seconds
D: setting network receive timeout to 60 seconds
D: Constructing Associate RQ PDU
D: PDU Type: Associate Accept, PDU Length: 184 + 6 bytes PDU header
D:   02  00  00  00  00  b8  00  01  00  00  41  43  4d  45  5f  53
D:   54  4f  52  45  20  20  20  20  20  20  41  43  4d  45  31  20
D:   20  20  20  20  20  20  20  20  20  20  00  00  00  00  00  00
D:   00  00  00  00  00  00  00  00  00  00  00  00  00  00  00  00
D:   00  00  00  00  00  00  00  00  00  00  10  00  00  15  31  2e
D:   32  2e  38  34  30  2e  31  30  30  30  38  2e  33  2e  31  2e
D:   31  2e  31  21  00  00  19  01  00  00  00  40  00  00  11  31
D:   2e  32  2e  38  34  30  2e  31  30  30  30  38  2e  31  2e  32
D:   50  00  00  3a  51  00  00  04  00  00  40  00  52  00  00  1b
D:   31  2e  32  2e  32  37  36  2e  30  2e  37  32  33  30  30  31
D:   30  2e  33  2e  30  2e  33  2e  36  2e  31  55  00  00  0f  4f
D:   46  46  49  53  5f  44  43  4d  54  4b  5f  33  36  31
D: Parsing an A-ASSOCIATE PDU
D: Association Parameters Negotiated:
D: ====================== BEGIN A-ASSOCIATE-AC =====================
D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
D: Our Implementation Version Name:   OFFIS_DCMTK_361
D: Their Implementation Class UID:    1.2.276.0.7230010.3.0.3.6.1
D: Their Implementation Version Name: OFFIS_DCMTK_361
D: Application Context Name:    1.2.840.10008.3.1.1.1
D: Calling Application Name:    ACME1
D: Called Application Name:     ACME_STORE
D: Responding Application Name: ACME_STORE
D: Our Max PDU Receive Size:    16384
D: Their Max PDU Receive Size:  16384
D: Presentation Contexts:
D:   Context ID:        1 (Accepted)
D:     Abstract Syntax: =VerificationSOPClass
D:     Proposed SCP/SCU Role: Default
D:     Accepted SCP/SCU Role: Default
D:     Accepted Transfer Syntax: =LittleEndianImplicit
D: Requested Extended Negotiation: none
D: Accepted Extended Negotiation:  none
D: Requested User Identity Negotiation: none
D: User Identity Negotiation Response:  none
D: ======================= END A-ASSOCIATE-AC ======================
I: Association Accepted (Max Send PDV: 16372)
I: Sending Echo Request (MsgID 1)
D: DcmDataset::read() TransferSyntax="Little Endian Implicit" 
I: Received Echo Response (Success)
I: Releasing Association
michael@einstein:~$

Much better! This time it worked ("I: Received Echo Response (Success)"), we setup up everything correctly. Now we can send images to the PACS.

Sending Images with storescu

This section descibres how to use the tool storescu and to get around some of its particular peculiarities. As always, have a look at the commandline options available by calling "storescu --help". This is the beginning of what is printed to the screen:

michael@einstein:~$ storescu --help
$dcmtk: storescu v3.6.1 DEV $

storescu: DICOM storage (C-STORE) SCU
usage: storescu [options] peer port dcmfile-in...

parameters:
  peer                         hostname of DICOM peer
  port                         tcp/ip port number of peer
  dcmfile-in                   DICOM file or directory to be transmitted
[...]
Thus storescu takes at least three parameters:
  • "peer" is the hostname or IP address you want to send images to
  • "port" is the TCP port number your server is listening.
  • "dcmfile-in" is the DICOM file or directory (not discussed here) that you like to send. Actually the "..." means you can provide more than one file or directory name here.

If you test with dcmqrscp and you have set it up like it is described above, you know after testing with echoscu (see above) that dcmqrscp is picky with AE Titles and we need to use the ones that we specified in dcmqrscp.cfg. Luckily DCMTK tries to name the commandline options consistently over different tools, so looking at storescu's help output reveals that the options are the same as for echoscu:

network options:
  application entity titles:
    -aet  --aetitle            [a]etitle: string
                               set my calling AE title (default: STORESCU)                                                                
    -aec  --call               [a]etitle: string                                                                                          
                               set called AE title of peer (default: ANY-SCP)

As a test image for transfer I use ct.dcm from the attached ZIP (originally taken and from Sebastien Barree, also linked here: http://support.dcmtk.org/redmine/projects/dcmtk/wiki/DICOM_Images).

So let's try to transfer the image to dcmqrscp. We add the correct AE Titles and also ask storescu to show us some processing output ("-v" for verbose; "-d" would provide too much information for the moment):

michael@einstein:~$ storescu localhost 11112 ct.dcm -aec ACME_STORE -aet ACME1 -v
I: checking input files ...
I: Requesting Association
I: Association Accepted (Max Send PDV: 16372)
I: Sending file: ct.dcm
I: Converting transfer syntax: Little Endian Explicit -> Little Endian Explicit
I: Sending Store Request (MsgID 1, CT)
XMIT: .................................
I: Received Store Response (Success)
I: Releasing Association
michael@einstein:~$ 

Et voila! The image was transferred to dcmqrscp ("Received Store Response (Success)"). We will try to query and retrieve it in the next sections. But first let's have a look at what can go wrong with storescu.

Troubleshooting

There are some common "mistakes" that happen wen using storescu which are easy to overcome. For demonstration, grab the file ct-compressed.dcm from the ZIP (or create it yourself using DCMTK's "dcmcjpeg") which is derived from the original ct.dcm that we have just send (for the experts: it has been compressed and put into a new Study and a new Series). Try the same command again, using the compressed file:

01 michael@einstein:~$ storescu localhost 11112 ct-compressed.dcm -aec ACME_STORE -aet ACME1 -v
02 I: checking input files ...
03 I: Requesting Association
04 I: Association Accepted (Max Send PDV: 16372)
05 I: Sending file: ct-compressed.dcm
06 I: Converting transfer syntax: JPEG Lossless, Non-hierarchical, 1st Order Prediction -> Little Endian Explicit
07 I: Sending Store Request (MsgID 1, CT)
08 XMIT: W: DIMSE Warning: (ACME1,ACME_STORE): sendMessage: unable to convert dataset from 'JPEG Lossless, Non-hierarchical, 1st Order Prediction' transfer syntax to 'Little Endian Explicit'
09 E: Store Failed, file: ct-compressed.dcm:
10 E: 0006:020e DIMSE Failed to send message
11 E: Store SCU Failed: 0006:020e DIMSE Failed to send message
12 I: Aborting Association
13 michael@einstein:~$

First it seems everything works fine, the association is accepted (line 04). DCMTK tries to decompress the file for transmission (line 06). This is a little weird: Why not send it the way it is, as JPEG-compressed DICOM image?! We'll find that out. storescu tells the underlying DCMTK libraries to send the file (line 07) but then a decompression error is encountered and the store fails (line 09). The remaining lines are just follow up errors leading to storescu aborting the association (line 12). To find out what happens, let's have a look at details from the association negotiation first; here are two snippets from the (long!) log that you get if you run in debug mode:

michael@einstein:~$ storescu localhost 11112 ct-compressed.dcm -aec ACME_STORE -aet ACME1 -d
01 D: $dcmtk: storescu v3.6.1 DEV $
02 D:
03 D: DcmDataDictionary: Loading file: /usr/local/share/dcmtk/dicom.dic
04 I: checking input files ...
05 D: Request Parameters:
06 D: ====================== BEGIN A-ASSOCIATE-RQ =====================
07 D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
08 D: Our Implementation Version Name:   OFFIS_DCMTK_361
09 D: Their Implementation Class UID:    
10 D: Their Implementation Version Name: 
11 D: Application Context Name:    1.2.840.10008.3.1.1.1
12 D: Calling Application Name:    ACME1
13 D: Called Application Name:     ACME_STORE
14 D: Responding Application Name: resp. AP Title
15 D: Our Max PDU Receive Size:    16384
16 D: Their Max PDU Receive Size:  0
17 D: Presentation Contexts:

[long list of presentation contexts follows, including:]

18 D:   Context ID:        41 (Proposed)
19 D:     Abstract Syntax: =CTImageStorage
20 D:     Proposed SCP/SCU Role: Default
21 D:     Proposed Transfer Syntax(es):
22 D:       =LittleEndianExplicit
23 D:   Context ID:        43 (Proposed)
24 D:     Abstract Syntax: =CTImageStorage
25 D:     Proposed SCP/SCU Role: Default
26 D:     Proposed Transfer Syntax(es):
27 D:       =BigEndianExplicit
28 D:       =LittleEndianImplicit

[some more presentation contexts, and then the response from the server:]

29 D: ====================== BEGIN A-ASSOCIATE-AC =====================
30 D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
31 D: Our Implementation Version Name:   OFFIS_DCMTK_361
32 D: Their Implementation Class UID:    1.2.276.0.7230010.3.0.3.6.1
33 : Their Implementation Version Name: OFFIS_DCMTK_361
34 D: Application Context Name:    1.2.840.10008.3.1.1.1
35 D: Calling Application Name:    ACME1
36 D: Called Application Name:     ACME_STORE
37 D: Responding Application Name: ACME_STORE
38 D: Our Max PDU Receive Size:    16384
39 D: Their Max PDU Receive Size:  16384
40 D: Presentation Contexts:

[the server now lists for every presentation context whether it is accepted or rejected, including]

41 D:   Context ID:        41 (Accepted)
42 D:     Abstract Syntax: =CTImageStorage
43 D:     Proposed SCP/SCU Role: Default
44 D:     Accepted SCP/SCU Role: Default
45 D:     Accepted Transfer Syntax: =LittleEndianExplicit
46 D:   Context ID:        43 (Accepted)
47 D:     Abstract Syntax: =CTImageStorage
48 D:     Proposed SCP/SCU Role: Default
49 D:     Accepted SCP/SCU Role: Default
50 D:     Accepted Transfer Syntax: =BigEndianExplicit

[plus more presentation contexts and the rest of the log]

From the full log you can see that storescu proposes not only two presentation contexts for CT images (lines 18-28) but also for MR images, Angiographic images, ECGs and so on. Why is that, seeing that we only want to send a CT image? The answer is that storescu is a rather dumb tool and proposes "everything" it can transfer without actually looking into the images it should transfer. And, as you can see in lines 22, 27 and 28, only uncompressed transfer syntaxes are porposed! storescu proposes only uncompressed transfer syntaxes by default, then loads our image and realizes that it is JPEG-compressed. It tries to find a matching Presentation Context, does not find one and therefore tries the best match (the uncompressed one, Little Endian, Context 41) and hopes that the underlying DCMTK libraries will do the right thing and decompress the image before sending. This is where it ultimately fails as the log above shows. The reason is that storescu does not configure the DCMTK libraries to do the decompression automatically (actually DCMTK could do that).

How can we fix this? Looking at storescu's options again, we find two options that look interesting:

    -xs   --propose-lossless      propose default JPEG lossless TS
                                  and all uncompressed transfer syntaxes
[...]
    -R    --required              propose only required presentation contexts
                                  (default: propose all supported)

Using "--propose-lossless" we force storescu to also propose the JPEG Lossless Transfer Syntax which is the one our file is compressed with. On top, we can use the "--required" option which will have the effect that storescu only proposes those SOP Classes that can be found in the image files provided on the commandline (in our case, that is only one image of SOP "Class CT Image Storage"). You will notice that the network log printed to the console is much shorter now. Let's try:

01 michael@einstein:~$ storescu localhost 11112 ct-compressed.dcm -aec ACME_STORE -aet ACME1 -d --required --propose-lossless
[...]
02 D: Presentation Contexts:
03 D:   Context ID:        1 (Proposed)
04 D:     Abstract Syntax: =CTImageStorage
05 D:     Proposed SCP/SCU Role: Default
06 D:     Proposed Transfer Syntax(es):
07 D:       =JPEGLossless:Non-hierarchical-1stOrderPrediction
[...]
08 W: DIMSE Warning: (ACME1,ACME_STORE): sendMessage: unable to convert dataset from 'JPEG Lossless, Non-hierarchical, 1st Order Prediction' transfer syntax to 'Little Endian Explicit'
09 E: Store Failed, file: ct-compressed.dcm:
10 E: 0006:020e DIMSE Failed to send message
11 E: Store SCU Failed: 0006:020e DIMSE Failed to send message
12 I: Aborting Association
13 michael@einstein:~$

Line 07 shows that this time JPEG Lossless is proposed. But, transmission fails again (Line 10), since again a decompression is attempted (Line 08). So where is the problem? Well, we did not look at what the server returns for our Presentation Context Proposal. Here it is:

[...]
01 D: Presentation Contexts:
02 D:   Context ID:        1 (Abstract Syntax Not Supported)
03 D:     Abstract Syntax: =CTImageStorage
04 D:     Proposed SCP/SCU Role: Default
05 D:     Accepted SCP/SCU Role: Default
[...]

The whole Presentation Context is rejected! So it is the server now that does not accept our CT Image Storage Proposal with JPEG Lossless as a Transfer Syntax (it accepts the other one where we propose uncompressed, though). So we also need to tweak dcmqrscp behaviour! Let's look at the dcmqrscp options ("dcmqrscp --help") and we can find:

    +xs   --prefer-lossless       prefer default JPEG lossless TS

OK, one last try. Restart dcmqrscp with option "--prefer-lossless" added. Do not forget the configuration file, i.e. run "dcmqrscp -c dcmqrscp.cfg --prefer-lossless". You can also add "-d" if you like to see what is going on at the server side. Then, let's try the same storescu call again:

01 michael@einstein:~$ storescu localhost 11112 ct-compressed.dcm -aec ACME_STORE -aet ACME1 -d --required --propose-lossless
[...]
02 D: ====================== BEGIN A-ASSOCIATE-AC =====================
03 D: Our Implementation Class UID:      1.2.276.0.7230010.3.0.3.6.1
04 D: Our Implementation Version Name:   OFFIS_DCMTK_361
05 D: Their Implementation Class UID:    1.2.276.0.7230010.3.0.3.6.1
06 D: Their Implementation Version Name: OFFIS_DCMTK_361
07 D: Application Context Name:    1.2.840.10008.3.1.1.1
08 D: Calling Application Name:    ACME1
09 D: Called Application Name:     ACME_STORE
10 D: Responding Application Name: ACME_STORE
11 D: Our Max PDU Receive Size:    16384
12 D: Their Max PDU Receive Size:  16384
13 D: Presentation Contexts:
14 D:   Context ID:        1 (Accepted)
15 D:     Abstract Syntax: =CTImageStorage
16 D:     Proposed SCP/SCU Role: Default
17 D:     Accepted SCP/SCU Role: Default
18 D:     Accepted Transfer Syntax: =JPEGLossless:Non-hierarchical-1stOrderPrediction
19 D:   Context ID:        3 (Accepted)
20 D:     Abstract Syntax: =CTImageStorage
21 D:     Proposed SCP/SCU Role: Default
22 D:     Accepted SCP/SCU Role: Default
23 D:     Accepted Transfer Syntax: =LittleEndianExplicit
24 D: Requested Extended Negotiation: none
25 D: Accepted Extended Negotiation:  none
26 D: Requested User Identity Negotiation: none
27 D: User Identity Negotiation Response:  none
28 D: ======================= END A-ASSOCIATE-AC ======================
29 I: Association Accepted (Max Send PDV: 16372)
30 I: Sending file: ct-compressed.dcm
31 D: DcmMetaInfo::checkAndReadPreamble() TransferSyntax="Little Endian Explicit" 
32 D: DcmDataset::read() TransferSyntax="JPEG Lossless, Non-hierarchical, 1st Order Prediction" 
33 I: Converting transfer syntax: JPEG Lossless, Non-hierarchical, 1st Order Prediction -> JPEG Lossless, Non-hierarchical, 1st Order Prediction
34 I: Sending Store Request (MsgID 1, CT)
35 D: ===================== OUTGOING DIMSE MESSAGE ====================
36 D: Message Type                  : C-STORE RQ
37 D: Message ID                    : 1
38 D: Affected SOP Class UID        : CTImageStorage
39 D: Affected SOP Instance UID     : 1.2.276.0.7230010.3.1.4.8323329.3844.1404313683.258283
40 D: Data Set                      : present
41 D: Priority                      : low
42 D: ======================= END DIMSE MESSAGE =======================
43 D: DcmDataset::read() TransferSyntax="Little Endian Implicit" 
44 I: Received Store Response
45 D: ===================== INCOMING DIMSE MESSAGE ====================
46 D: Message Type                  : C-STORE RSP
47 D: Presentation Context ID       : 1
48 D: Message ID Being Responded To : 1
49 D: Affected SOP Class UID        : CTImageStorage
50 D: Affected SOP Instance UID     : 1.2.276.0.7230010.3.1.4.8323329.3844.1404313683.258283
51 D: Data Set                      : none
52 D: DIMSE Status                  : 0x0000: Success
53 D: ======================= END DIMSE MESSAGE =======================
54 I: Releasing Association

This time transmission worked! The server accepted our JPEG Lossless proposal (line 14), no decompression was necessary (line 32 and 33), the C-STORE with the image was transferred successfully (line 52) and in the end the connection was released (line 54).

As a takeaway, remember:
  • Use logging option "-d" to see what actually happens during association negotiation. If you can, on server and on client side.
  • If you transfer compressed DICOM files, enable one of the various --propose-xxx options in storescu
    • Note that you can supply storescu with a association configuration file which lets you specific exactly which transfer syntaxes to propose. There is an example configuration included in DMCTK (storescu.cfg).
  • Also, use the analogous "--prefer-xxx" option for dcmqrscp!

There is a tool that also acts as a Storage SCU but is more intelligent than storescu is. It is called dcmsend and is also part of recent DCMTK versions (snapshot versions after 3.6.0).

Querying a PACS with findscu

Now that we have two images registered in dcmqrscp, we try to query for them using the findscu tool. This means that we like to search the PACS (dcmqrscp) database for Patients, Studies, Series and Images. For retrieving we need another tool (movescu, discussed later). The requirements of the Query SOP Classes in DICOM are not explained here. In particular, the query keys that must be provided so that the query is well-formed and DICOM-conformant is beyond the scope of this tutorial. Let's look at findscu's help page and pick the options we need:

01 michael@einstein:~$ findscu --help
02 $dcmtk: findscu v3.6.1 DEV $
03 
04 findscu: DICOM query (C-FIND) SCU
05 usage: findscu [options] peer port [dcmfile-in...]
06
07 parameters:
08   peer                         hostname of DICOM peer
09   port                         tcp/ip port number of peer
10   dcmfile-in                   DICOM query file(s)
[...]
11   override matching keys:
12     -k    --key                [k]ey: gggg,eeee="str", path or dict. name="str" 
13                                override matching key
14   query information model:
15     -W    --worklist           use modality worklist information model (default)
16     -P    --patient            use patient root information model
17     -S    --study              use study root information model
18     -O    --psonly             use patient/study only information model
19   application entity titles:
20     -aet  --aetitle            [a]etitle: string
21                                set my calling AE title (default: FINDSCU)
22     -aec  --call               [a]etitle: string
23                                set called AE title of peer (default: ANY-SCP)
[...]

Parameters are peer, port (you know those from above) and DICOM input files containing the queries we like to send. Note that the parameter "dcmfile-in" is printed in square brackets; this means it is optional and we can specify the query also in another way, in particular by using the "--key" option. We will see how this works. Before, look at the list under "query information model:". This lets findscu choose the SOP Class it should propose to the PACS. Besides the Query SOP Classes (Patient Root, Study Root and Patient/Study Only), also Worklist is supported. We will use the Query SOP Class that is supported by most PACS, the Study Root-related Query SOP Class which can be selected by providing the "--study" option. Finally, we have to specify the AE Titles again, using the same options you already know from echoscu and storescu. Here is a query that will search for all Studies on the PACS, printing their Study Date, Study Description and Study Instance UID:

01 michael@einstein:~$ findscu -v -S -aec ACME_STORE -aet ACME1 localhost 11112 -k QueryRetrieveLevel=STUDY -k StudyDate -k StudyDescription -k StudyInstanceUID
02 I: Requesting Association
03 I: Association Accepted (Max Send PDV: 16372)
04 I: Sending Find Request (MsgID 1)
05 I: Request Identifiers:
06 I: 
07 I: # Dicom-Data-Set
08 I: # Used TransferSyntax: Little Endian Explicit
09 I: (0008,0020) DA (no value available)                     #   0, 0 StudyDate
10 I: (0008,0052) CS [STUDY]                                  #   6, 1 QueryRetrieveLevel
11 I: (0008,1030) LO (no value available)                     #   0, 0 StudyDescription
12 I: (0020,000d) UI (no value available)                     #   0, 0 StudyInstanceUID
13 I: 
14 I: ---------------------------
15 I: Find Response: 1 (Pending)
16 I: 
17 I: # Dicom-Data-Set
18 I: # Used TransferSyntax: Little Endian Explicit
19 I: (0008,0020) DA [20131231]                               #   8, 1 StudyDate
20 I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
21 I: (0008,0054) AE [ACME_STORE]                             #  10, 1 RetrieveAETitle
22 I: (0008,1030) LO [Multiple Fractures from Skiing]         #  30, 1 StudyDescription
23 I: (0020,000d) UI [1.2.276.0.7230010.3.1.2.8323329.4723.1404318646.59559] #  54, 1 StudyInstanceUID
24 I: 
25 I: ---------------------------
26 I: Find Response: 2 (Pending)
27 I: 
28 I: # Dicom-Data-Set
29 I: # Used TransferSyntax: Little Endian Explicit
30 I: (0008,0020) DA [20140101]                               #   8, 1 StudyDate
31 I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
32 I: (0008,0054) AE [ACME_STORE]                             #  10, 1 RetrieveAETitle
33 I: (0008,1030) LO [General Checkup ]                       #  16, 1 StudyDescription
34 I: (0020,000d) UI [2.16.840.1.113662.2.1.1519.11582.1990505.1105152] #  48, 1 StudyInstanceUID
35 I: 
36 I: Received Final Find Response (Success)
37 I: Releasing Association
38 michael@einstein:~$

Note that I used "-v" for verbose mode instead of "-d" for debug since we do not like to see the association negotiation again, but the query response from the server of course. If you query another PACS than dcmqrscp, run in debug mode if you encounter problems and analyze the Presentation Contexts as we have done this for storescu above!

Let's go through the console output: findscu prints the request (line 05 to line 12) that we are sending. It consists exactly of those values we provided to finsdcu using the "--key" option. As you notice in the call, we repeat the "--key" option as often as we need query keys. Thus the values do not override each other in the case of findscu but instead each of them is used to assemble the query sent to the SCP. We receive two actual responses (line 15, line 26) from the server. Each of them includes they query keys we asked for (Study Date, Query Retrieve Level, Study Description and Study Instance UID. This means there are two Studies matching our query (which selected all Studies on the PACS). Additionally, the server adds extra information to each query result, the "Retrieve AE Title": This tells us from which AE Title we can retrieve the Study if we want (we do that later). Other servers may include a few additional result attributes; however, the general rule is that you only get information back that you explicitly asked for. Each result carrying data is sent with the status "Pending". According to DICOM, the last result (in case everything worked fine) always has the status "Success" (see line 36) and does not have any result data included. After receiving the final result, findscu releases the association.

We do not explain the general Query protocol here. For details look at the Query SOP Classes that are specified in part 4 of the DICOM standard.

Retrieving from a PACS using movescu

Now we can check a DICOM connection (echoscu), start a PACS (dcmqrscp), store to it (storescu) and query for Studies. The last thing that is missing is actually retrieving data from the PACS. There are two ways to do that, the "C-MOVE" approach or the "C-GET" approach. C-MOVE is more flexible and the most common one found in practice; that's why we show it here. In DCMTK the tool "movescu" implements the C-MOVE approach. For each of the three Query SOP Classes (Patient Root, Study Root and Patient/Study Only) there is also a related C-MOVE, and a related C-GET SOP Class. We use the C-MOVE Study Root Retrieve SOP Class here (note that the exact SOP Class names are a little bit different but have been replaced for readability).

Using C-MOVE one can order a set of images from the PACS. The PACS then transfers them using the Storage SOP Classes that are also used by storescu (see above). Thus, the receiver of images ordered by a C-MOVE command must be a a server (SCP) of the required Storage SOP Classes. In order to receive the images yourself, you must tell the PACS your own AE Title which is contained in the C-MOVE order message as the "Move Destination". Now, the special thing about the C-MOVE approach is that you can tell the PACS not only your own AE Title but also any other AE Title on the DICOM network! That way you can not only ask the PACS to send the images to yourself but also to any Storage-enabled DICOM server by providing its AE Title.

A small disadvantage comes with this flexibility: The PACS must somehow know where it can find the selected "Move Destination" on the network, i.e. it must map the AE Title to an IP address or hostname, and to a TCP port number. This means that any receiver of images must be configured on the PACS first. Luckily we have already done that for dcmqrscp in dcmqrscp.cfg where we tell dcmqrscp about two AE Titles:

HostTable BEGIN
acme1           = (ACME1, localhost, 1234)
acme2           = (ACME2, localhost, 5678)
acmeCTcompany   = acme1, acme2

We will use ACME1 as the receiver. Therefore we need to start a DICOM Storage SCP. We could use DCMTK's "storescp" tool for that, but we can make it even simpler: movescu is not only able to send the C-MOVE request but also has a built-in storage server that can receive images. We just have to tell movescu to start it. Let's look at some selected movescu options that will help us:

01 michael@einstein:~$ movescu --help
02 $dcmtk: movescu v3.6.1 CVS $
03
04 movescu: DICOM retrieve (C-MOVE) SCU
05 usage: movescu [options] peer port [dcmfile-in...]
[...]
06   override matching keys:
07    -k    --key                  [k]ey: gggg,eeee="str" or dict. name="str" 
08                                 override matching key
09   query information model:
10     -P    --patient              use patient root information model (default)
11     -S    --study                use study root information model
12     -O    --psonly               use patient/study only information model
13   application entity titles:
14     -aet  --aetitle              [a]etitle: string
15                                  set my calling AE title (default: MOVESCU)
16     -aec  --call                 [a]etitle: string
17                                  set called AE title of peer (default: ANY-SCP)
18     -aem  --move                 [a]etitle: string
19                                  set move destinat. AE title (default: MOVESCU)
[...]
20     +xs   --prefer-lossless      prefer default JPEG lossless TS
[...]
21   port for incoming network associations:
22           --no-port              no port for incoming associations (default)
23     +P    --port                 [n]umber: integer
24                                  port number for incoming associations
[...]
25   general:
26     -od   --output-directory     [d]irectory: string (default: ".")
27                                  write received objects to existing directory d
[...]

As parameters, movescu expects the same information as findscu: peer and port of the server to retrieve from and optionally one or more query files that contain the retrieve keys defining what to download (all in line 05). Instead we also can use the --key option as we did for findscu (line 07). As for the Query task with findscu, we need to tell movescu which SOP Class to use. We select again the Study level SOP class (i.e. use option "--study", line 11). We also already know how to work with the AE Title options "-aet" and "-aec" (line 14 and 16); we will use the same values as for echoscu and findscu.

However there is one new AE Title option: "--move" (line 18). Here we set the "Move Destination" that tells the PACS (dcmqrscp) where to send the images to. Per default this value is set to "MOVESCU". However, dcmqrscp has never been configured to know a system called MOVESCU. Instead, we configured ACME1 (and ACME2), which is the one we will use.

Further down, the option "--port" (line 23) is described. This is not the port we defined as part of the parameters, i.e. not the port where dcmqrscp is listening. Instead, it is the port we'd like to receive the images on, i.e. where we tell movescu to start an SCP that listens for Storage SOP Class requests. Remember that we can use movescu to tell the server (dcmqrscp) to move the images to a third party system by setting a Move Destination that dcmqrscp knows. In that case we would not provide the "--port" option but instead the "--no-port" default option is enabled, so not movescu would expect to receive the image but the system responsible for the Move Destination AE Title provided. Since we want to represent the system "ACME1", we must use the port that we configured in dcmqrscp's configuration file, which is "1234" for ACME1.

On top, you can tell movescu using option "--output-directory" (line 26) or shortly "-od" that any images received should be stored in the given directory. You can provide any writeable, existing directory name here. If you do not provide the option, the current working directory will be used to store the any images received.

Overall we end up with the following call to grab the STUDY containg ct.dcm:

michael@einstein:~$ movescu -v -S -aec ACME_STORE -aet ACME1 -aem ACME1 --port 1234 -od /tmp/ localhost 11112 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID=2.16.840.1.113662.2.1.1519.11582.1990505.1105152

Note that I copied one of the Study Instance UIDs of the findscu results into the Retrieve command in order to specify the Study that should be downloaded. Let's see what happens:

01 michael@einstein:~$ movescu -v -S -aec ACME_STORE -aet ACME1 -aem ACME1 --port 1234 -od /tmp/ localhost 11112 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID=2.16.840.1.113662.2.1.1519.11582.1990505.1105152
02 I: Requesting Association
03 I: Association Accepted (Max Send PDV: 16372)
04 I: Sending Move Request: MsgID 1
05 I: Request:
06 I:
07 I: # Dicom-Data-Set
08 I: # Used TransferSyntax: Unknown Transfer Syntax
09 I: (0008,0052) CS [STUDY]                                  #   6, 1 QueryRetrieveLevel
10 I: (0020,000d) UI [2.16.840.1.113662.2.1.1519.11582.1990505.1105152] #  48, 1 StudyInstanceUID
11 I:
12 I: Received Store Request: MsgID 1, (CT)
13 RECV: .................................
14 I: Move Response 1:
15 I: ===================== INCOMING DIMSE MESSAGE ====================
16 I: Message Type                  : C-MOVE RSP
17 I: Message ID Being Responded To : 1
18 I: Affected SOP Class UID        : MOVEStudyRootQueryRetrieveInformationModel
19 I: Remaining Suboperations       : 0
20 I: Completed Suboperations       : 1
21 I: Failed Suboperations          : 0
22 I: Warning Suboperations         : 0
23 I: Data Set                      : none
24 I: DIMSE Status                  : 0xff00: Pending
25 I: ======================= END DIMSE MESSAGE =======================
26 I: ===================== INCOMING DIMSE MESSAGE ====================
27 I: Message Type                  : C-MOVE RSP
28 I: Message ID Being Responded To : 1
29 I: Affected SOP Class UID        : MOVEStudyRootQueryRetrieveInformationModel
30 I: Remaining Suboperations       : none
31 I: Completed Suboperations       : 1
32 I: Failed Suboperations          : 0
33 I: Warning Suboperations         : 0
34 I: Data Set                      : none
35 I: DIMSE Status                  : 0x0000: Success
36 I: ======================= END DIMSE MESSAGE =======================
37 I: Releasing Association
38 michael@einstein:~$

Wow, that worked out of the box! After association negotation the request is constructed from our commandline input (line 05 to line 10) and sent over the wire as a C-MOVE Request (line 04). As a result, dcmqrscp starts a separate connection to our storage port 1234 (line 12), and the SOP Class is CT Image Storage. The dots in line 13 are printed for every network data package (PDU) that arrives. The message received is a "C-STORE Request" and we send back a "C-STORE Response" with "Success". You can see the details if you like if you enable "--debug" instead of "-v".

After dcmqrscp has sent the (only) image of the given study, it sends a C-MOVE Response (line 26-36) telling us that it has sent 1 image successfully (line 31). The overall status is "Success" (line 36) so all images requested have been transferred successfully. Afterwards, movescu releases the association.

movescu stores the images to the directory you specified with (-od) as described above. The files are named after their modality type (SOP Class) and world-wide unique SOP Instance UID, since the original filename (ct.dcm) is never transferred over the wire. In our example the retrieved file will have the filename "CT.2.16.840.1.113662.2.1.4519.41582.4105152.419990505.410523251".

If you like to retrieve the other Study too, go ahead and just change the Study Instance UID. Also note that we now must retrieve a JPEG lossless image, make sure movescu accepts it by providing the "--prefer-lossless" or shortly "-xs" option! Also dcmqrscp must be forced to actually propose JPEG lossless when sending the images (otherwise it will have the same problem with decompression like storescu, see above!): Restart the server and use the command: "dcmqrscp -c dcmqrscp.cfg +xs -xs". Note that "+xs" on the server means that is should accept storage of JPEG-lossless compressed files. The "-xs" means that is should propose the JPEG Lossless Transfer Syntax in order to be able to send JPEG Lossless files to a storage receiver. Now call movescu with the Study Instance UID of the Study containing the original ct-compressed.dcm image:

michael@einstein:~$ movescu -v -S -aec ACME_STORE -aet ACME1 -aem ACME1 --port 1234 -od /tmp/ localhost 11112 -k QueryRetrieveLevel=STUDY -k StudyInstanceUID=1.2.276.0.7230010.3.1.2.8323329.4723.1404318646.59559 +xs

That's it :)

Acknowledgements

This tutorial was supported through the NA-MIC project . NA-MIC is a national research center supported by grant U54 EB005149 from the NIBIB NIH HHS Roadmap for Medical Research Program.

Written my Michael Onken (DCMTK Team and Open Connections GmbH).