PACS Debugging with DCMTK¶
- Table of contents
- 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¶
- Below we start with setting up the DCMTK PACS server dcmrscp. Of course you can and may want to use your own server. There is even one on the Internet" that is offered by a DICOM colleague at Medical Connections from Wales.
- This page has some DICOM test images" if you want to play around with other examples.
- Of course, if not already done, grab a copy of DCMTK".
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).