DICOM Basics using Java - Query and Retrieve Operations (C-GET)
How the C-GET Composite Service Works
This article is part of my series of articles on the DICOM standard that I am currently working on (a number of them have already been completed). If you are totally new to DICOM, please have a quick look at my earlier article titled “Introduction to the DICOM Standard” for a quick introduction to the standard. It may also be useful to look at my other tutorials that have been completed so far to get up to speed on a number of topics including DICOM Encoding, SOPs and IODs. Before you dive into this article, please have a look at my previous DICOM networking-related tutorials on DICOM Verification and on DICOM Associations as a basic understanding of those topics is required to understand the material that is covered here. This tutorial also assumes that you know the basics of C# or any equivalent object-oriented language such as Java or C++. A basic understanding of networking will also be useful to have but is not mandatory.
Introduction
In this tutorial, we are going to continue exploring how query retrieval operations are implemented using the DICOM standard. We have looked at two other composite operations in this group already. They are namely, the the C-FIND and the the C-MOVE operations. C-FIND, as you will recall, allows you to query for DICOM data (images, structured reports, etc), and the C-MOVE operation allow you to then retrieve this data (as well as move this data to a specified destination). We are now going to look at one more way to retrieve DICOM data using a more aptly named operation called the C-GET composite operation which works slightly different under the covers than the C-MOVE operation. C-GET is not as popular as C-MOVE, but very useful neverthless. Most users coming from a non-healthcare background will also be able to relate to it much more easily since it operates very much like a GET request works when using the HTTP protocol.
The PixelMed Java DICOM Toolkit - Quick Overview
For the purposes of illustrating many aspects of DICOM that I plan to cover in this tutorial series, I will be using a freely available and powerful DICOM toolkit called PixelMed Java DICOM Toolkit. This is a completely stand-alone DICOM toolkit that provides functionality for DICOM file and directory processing, image viewing as well as DICOM networking-related operations. This toolkit is completely free for both commercial or non-profit use. It is well documented and also has a small discussion forum and mailing list for users. The list of features contained within this toolkit is quite comprehensive. Please keep in mind that the use of this toolkit in my tutorial does not in anyway imply my official endorsement of it for implementing a production application. Every situation is unique, and only you are ultimately in the best position to decide that. This article is also not meant to be a tutorial on this toolkit, and my focus here is simply to tie DICOM theory to what a practical (although simple) implementation might look like. So, if your goal is to learn how to use the PixelMed library, I would encourage you to visit its website or check out the discussion forum or StackOverflow discussion pages for any assistance.
Before We Get Started…
Much like my previous programming examples, I will use the most bare minimum code and approach to help illustrate the concepts that I cover in this tutorial. This means that the code I write here is best suited to simply show the concept that I am trying to explain and is not necessarily the most efficient code to deploy in real life and in your production application.
To get started, you will need to configure a few things on your machine including a Java development environment as well as the PixelMed toolkit before you can run the example if you want to try this out yourself.
- Download and install the Eclipse Java IDE from here (or use any other IDE you prefer)
- Download the PixelMed toolkit library from here
- Ensure that the PixelMed.jar library is included in your Java project’s class path (some examples may require additonal runtime dependencies such as JAI Image IO Tools that can be found on PixelMed software download. Look for a tar compressed file called pixelmedjavadicom_dependencyrelease.YYYYMMDD.tar.bz2 or something similar)
- You can find the source code and images used in this tutorial on GitHub
- You can download more DICOM images from this site if you want as well
PACS Server Requirement
In addition to the tools described above, you will also need a DICOM server to execute some of the operations described in this tutorial. If you don't have access to one, you have one of two options. First option is to use Dr. Dave Harvey's free online PACS server provided here. Dr. Dave Harvey is a radiologist by background who runs MedicalConnections, a software company which provides medical imaging-related technology consulting services for clients as well toolkits for developers in the DICOM space. Although I have never used his toolkit, I have reached out to him for help on DICOM matters in the past, and he was kind enough to point me to some very useful material to read or look at regarding DICOM. So, check out his toolkit if you are looking for a commercial solution for your DICOM requirement that is compatible with the Microsoft platform. Another option is to download one of the many open source PACS servers available on the Internet. Orthanc Server is one such tool. Please see my article on getting started with Orthanc Server for more information.
“I dream of lost vocabularies that might express some of what we no longer can..” ~ Jack Gilbert, The Great Fires
Retrieving DICOM Data using C-GET
C-GET is another (and more 'modern') way for retrieval of DICOM data that you may be interested in. Unlike the C-MOVE operation (with a slightly counter-intuitive name) that lets you both retrieve data but also send data to an entirely different destination, the C-GET operation does exactly what you think it would (or should) which is to get the DICOM data that you are interested in. It also uses a single DICOM association (instead of two like C-MOVE does) to do the data retrieval operation making it easier to use from a configuration standpoint since there are fewer ports to deal with.
Despite its more intuitive name, and even though data can be sent data back to the caller using the same DICOM association, it may be a bit surprising to learn that the C-MOVE operation still remains the more popular method used for data retrieval. There are primarily two reasons for this. The first reason is that C-MOVE will only send data to callers who are already registered in the AE lookup tables making PACS administrators usually a bit more trusting of this approach. The second reason for C-MOVE's popularity is because it is simpler to implement than C-GET which requires far more negotiations and role switching between the same devices since all of this activity must happen over a single connection. For these reasons, a vast number of PACS manufacturers have historically not supported the C-GET operation*. Still, C-GET has a number of uses in diagnostic imaging such as when pulling images over the Internet from a PACS system deployed within a hospital system (which C-MOVE can only do with a lot of difficulty since additional ports and AE titles have to configured), and is also used in DICOM Modality Worklist operations (which I will cover in a separate tutorial).The diagram below should hopefuly illustrate how the C-GET operation works.
From a DICOM communications perspective, a DIMSE command ('C-GET-RQ') coupled with a DICOM IOD object containing the criteria for the data that needs to be moved to a specified destination is transmitted from the client (C-GET-SCU) to the service provider (C-GET SCP). The criteria is specified through the "matching keys" attribute that we saw during our tutorials on C-FIND and C-MOVE. In addition, query/retrieve level information is also transmitted which helps specify what we want to retrieve (an entire study, series, image, etc). A character set may sometimes also be specified to control the character encoding of the value representations of the DICOM data that is returned. The C-GET SCP then switches roles to a C-STORE SCU and sends a 'C-STORE-RQ' command and any data matching the criteria to the specified destination. The C-GET SCP continues to communicate the status of the C-STORE operation such as the number of files transferred, number of files pending for transmission, any warnings and errors that arise during the operation, etc. The C-GET SCU may also initiate a cancel operation (using 'C-CANCEL-GET-RQ') at anytime at which time any C-STORE operations that are in progress will also be canceled, and a status of 'cancelled' in communicated back to the C-GET SCU. The C-GET request and response structures are shown below for reference. Please see the official DICOM documentation for more details as that is pretty all the theory I can cover in this article.
Example of C-GET Operation
Let us proceed to look at a quick example using the PixelMed toolkit to perform the C-GET SCU operation. I am going to connect to the public DICOM server made available by MedicalConnections information on which is provided here. Please read the instructions provided in that link to configure the code as necessary for your particular situation. I am going to send a request to it to first find any DICOM studies pertaining to a specific patient, and have it send any matched studies sent to my Java client. In the overall operation, my Java client is acting in the role of a C-GET SCU and switches roles to C-STORE SCP as needed, and the remote MedicalConnections DICOM server is acting as both a C-GET SCP and as a C-STORE SCU. The code should be pretty self explanatory if you understood the theory behind this operation which was explained in the previous sections.
package com.saravanansubramanian.dicom.pixelmedtutorial;
import java.io.File;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
import com.pixelmed.dicom.Attribute;
import com.pixelmed.dicom.AttributeList;
import com.pixelmed.dicom.AttributeTag;
import com.pixelmed.dicom.CodeStringAttribute;
import com.pixelmed.dicom.DicomException;
import com.pixelmed.dicom.SOPClass;
import com.pixelmed.dicom.SpecificCharacterSet;
import com.pixelmed.dicom.StoredFilePathStrategy;
import com.pixelmed.dicom.TagFromName;
import com.pixelmed.dicom.UniqueIdentifierAttribute;
import com.pixelmed.network.DicomNetworkException;
import com.pixelmed.network.FindSOPClassSCU;
import com.pixelmed.network.GetSOPClassSCU;
import com.pixelmed.network.IdentifierHandler;
import com.pixelmed.network.ReceivedObjectHandler;
public class DicomCGetFunctionalityDemo {
public static void main(String arg[]) {
//Summary of what we are doing here:
//1. Perform a C-FIND operation for all studies matching a specific patient
//2. For each study found, retrieve all DICOM objects belonging to the study
//3. The C-FIND operation should help us find and later specify the SOP classes
// that we need to provide for the C-GET operation
//4. As each file is received, write the information about the incoming data to the console
try {
// use the default character set for VR encoding - override this as necessary
SpecificCharacterSet specificCharacterSet = new SpecificCharacterSet((String[]) null);
AttributeList identifier = new AttributeList();
// build the attributes that you would like to retrieve as well as passing in
// any search criteria
identifier.putNewAttribute(TagFromName.QueryRetrieveLevel).addValue("STUDY"); // specific query root
identifier.putNewAttribute(TagFromName.PatientName, specificCharacterSet).addValue("Bowen*");
identifier.putNewAttribute(TagFromName.PatientID, specificCharacterSet);
identifier.putNewAttribute(TagFromName.PatientBirthDate);
identifier.putNewAttribute(TagFromName.PatientSex);
identifier.putNewAttribute(TagFromName.StudyInstanceUID);
identifier.putNewAttribute(TagFromName.SOPInstanceUID);
identifier.putNewAttribute(TagFromName.StudyDescription);
identifier.putNewAttribute(TagFromName.StudyDate);
identifier.putNewAttribute(TagFromName.SOPClassesInStudy);
// retrieve all studies belonging to patient with name 'Bowen'
new FindSOPClassSCU("www.dicomserver.co.uk", 104, "MEDCONN", "OurFindScu",
SOPClass.StudyRootQueryRetrieveInformationModelFind, identifier, new OurFindHandler());
} catch (Exception e) {
e.printStackTrace(System.err); // in real life, do something about this exception
System.exit(0);
}
}
}
class OurFindHandler extends IdentifierHandler {
private static String GetSCP_Address = "www.dicomserver.co.uk";
private static String GetSCP_AE_Title = "MEDCONN";
private static int GetSCP_Port_Number = 104;
private static String GetSCU_AE_TITLE = "JavaClient";
File pathToStoreIncomingDicomFiles = new File("c:\\RetrievedDicomData");
public static int resultsFound = 0;
@SuppressWarnings("unchecked")
@Override
public void doSomethingWithIdentifier(AttributeList attributeListForFindResult) throws DicomException {
resultsFound++;
System.out.println("Matched result:" + attributeListForFindResult);
String studyInstanceUID = attributeListForFindResult.get(TagFromName.StudyInstanceUID)
.getSingleStringValueOrEmptyString();
System.out.println("studyInstanceUID of matched result:" + studyInstanceUID);
Set<String> setofSopClassesExpected = new HashSet<String>();
Attribute sopClassesInStudy = attributeListForFindResult.get(TagFromName.SOPClassesInStudy);
if (sopClassesInStudy != null) {
String[] sopClassesInStudyList = sopClassesInStudy.getStringValues();
for (String sopClassInStudy : sopClassesInStudyList) {
setofSopClassesExpected.add(sopClassInStudy);
}
} else {
//if SOP class data for study is not found, then supply all storage SOP classes
setofSopClassesExpected = (Set<String>) SOPClass.getSetOfStorageSOPClasses();
}
try {
AttributeList identifier = new AttributeList();
{
AttributeTag tag = TagFromName.QueryRetrieveLevel;
Attribute attribute = new CodeStringAttribute(tag);
attribute.addValue("STUDY");
identifier.put(tag, attribute);
}
{
AttributeTag tag = TagFromName.StudyInstanceUID;
Attribute attribute = new UniqueIdentifierAttribute(tag);
attribute.addValue(studyInstanceUID);
identifier.put(tag, attribute);
}
//please see PixelMed documentation if you want to dig deeper into the parameters and their relevance
new GetSOPClassSCU(GetSCP_Address,
GetSCP_Port_Number,
GetSCP_AE_Title,
GetSCU_AE_TITLE,
SOPClass.StudyRootQueryRetrieveInformationModelGet,
identifier,
new IdentifierHandler(), //override and provide your own handler if you need to do anything else
pathToStoreIncomingDicomFiles,
StoredFilePathStrategy.BYSOPINSTANCEUIDINSINGLEFOLDER,
new OurCGetOperationStoreHandler(),
setofSopClassesExpected,
0,
true,
false,
false);
} catch (Exception e) {
System.out.println("Error during get operation" + e); // in real life, do something about this exception
e.printStackTrace(System.err);
}
}
}
class OurCGetOperationStoreHandler extends ReceivedObjectHandler {
@Override
public void sendReceivedObjectIndication(String filename, String transferSyntax, String calledAetTitle)
throws DicomNetworkException, DicomException, IOException {
System.out.println("Incoming data from " + calledAetTitle + "...");
System.out.println("filename:" + filename);
System.out.println("transferSyntax:" + transferSyntax);
}
}
19:05:36,356 DEBUG [main] AssociationInitiator:330 - establishing Association
16:44:30,921 DEBUG [main] FindSOPClassSCU:330 - Association[0]: Hostname: www.dicomserver.co.uk
Association[0]: Port: 104
Association[0]: Called AE Title: MEDCONN
Association[0]: Calling AE Title: OurFindScu
[Presentation Context ID: 0x1 (result 0x0 - acceptance)
Abstract Syntax:
1.2.840.10008.5.1.4.1.2.2.1
Transfer Syntax(es):
1.2.840.10008.1.2.1
, Presentation Context ID: 0x3 (result 0x0 - acceptance)
Abstract Syntax:
1.2.840.10008.5.1.4.1.2.2.1
Transfer Syntax(es):
1.2.840.10008.1.2
, Presentation Context ID: 0x5 (result 0x0 - acceptance)
Abstract Syntax:
1.2.840.10008.5.1.4.1.2.2.1
Transfer Syntax(es):
1.2.840.10008.1.2.1
]
16:44:30,923 DEBUG [main] FindSOPClassSCU:347 - request identifier
(0x0008,0x0018) SOPInstanceUID VR=<UI> VL=<0x0> <>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x0> <>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x5> <STUDY>
(0x0008,0x0062) SOPClassesInStudy VR=<UI> VL=<0x0> <>
(0x0008,0x1030) StudyDescription VR=<LO> VL=<0x0> <>
(0x0010,0x0010) PatientName VR=<PN> VL=<0x6> <Bowen*>
(0x0010,0x0020) PatientID VR=<LO> VL=<0x0> <>
(0x0010,0x0030) PatientBirthDate VR=<DA> VL=<0x0> <>
(0x0010,0x0040) PatientSex VR=<CS> VL=<0x0> <>
(0x0020,0x000d) StudyInstanceUID VR=<UI> VL=<0x0> <>
16:44:30,925 DEBUG [main] FindSOPClassSCU:347 - Using context ID 5
16:44:30,932 DEBUG [main] FindSOPClassSCU:330 - waiting for PDUs
16:44:31,417 DEBUG [main] CompositeResponseHandler:330 - sendPDataIndication():
16:44:31,418 DEBUG [main] CompositeResponseHandler:330 - sendPDataIndication(): last fragment of command seen
16:44:31,423 DEBUG [main] FindSOPClassSCU:347 - evaluateStatusAndSetSuccess:
(0x0000,0x0000) CommandGroupLength VR=<UL> VL=<0x4> [0x4c]
(0x0000,0x0002) AffectedSOPClassUID VR=<UI> VL=<0x1c> <1.2.840.10008.5.1.4.1.2.2.1
Matched result:(0x0008,0x0005) SpecificCharacterSet VR=<CS> VL=<0x0> <>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x8> <20191017>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x6> <STUDY >
(0x0008,0x0054) RetrieveAETitle VR=<AE> VL=<0x8> <MEDCONN >
(0x0008,0x0062) SOPClassesInStudy VR=<UI> VL=<0x20> <1.2.840.10008.5.1.4.1.1.77.1.5.1>
(0x0008,0x1030) StudyDescription VR=<LO> VL=<0x0> <>
(0x0010,0x0010) PatientName VR=<PN> VL=<0x12> <Bowen^William^^Dr >
(0x0010,0x0020) PatientID VR=<LO> VL=<0x6> <PAT004>
(0x0010,0x0030) PatientBirthDate VR=<DA> VL=<0x0> <>
(0x0010,0x0040) PatientSex VR=<CS> VL=<0x0> <>
(0x0020,0x000d) StudyInstanceUID VR=<UI> VL=<0x1a> <1.2.826.0.1.3680043.11.106>
studyInstanceUID of matched result:1.2.826.0.1.3680043.11.106
Incoming data from MEDCONN...
filename:c:\RetrievedDicomData\2.25.101703809854595919801950834747690813074
transferSyntax:1.2.840.10008.1.2.1
Incoming data from MEDCONN...
filename:c:\RetrievedDicomData\2.25.129202192333655564630684515562976033020
transferSyntax:1.2.840.10008.1.2.1
Incoming data from MEDCONN...
filename:c:\RetrievedDicomData\2.25.270772719488604832156479486332443261838
transferSyntax:1.2.840.10008.1.2.1
Incoming data from MEDCONN...
filename:c:\RetrievedDicomData\2.25.116421094412591474296935738553064334108
transferSyntax:1.2.840.10008.1.2.1
“The seeds we sow today will grow to serve as shades for weary travellers tomorrow.” ~ Nike Thaddeus
A portion of the output results seen in the console window is shown above. All DICOM objects pertaining to the matched study for the patient with the name "Bowen" are returned to the C-GET SCU, and any additional processing can be performed on these files as needed. Often, when the DICOM images are retrieved using the C-GET operation, they are viewed on high resolution monitors using either commercial or open source DICOM viewing software. From this, a radiologist may create dictations and other reports, and these in turn are pushed to other systems to enable additional patient care-related activities.
Conclusion
This concludes the article on how the C-GET composite operation works. The C-FIND, C-MOVE and C-GET composite services that we have looked at in the last three articles of my DICOM series form the foundation for all query and retrieval operations within any DICOM-related workflows. In the next tutorial in this series, I will cover another composite operation called "C-STORE". This service ensures safe archival of data that is captured in various DICOM modalities such as CTs and MRI machines. See you then!
*You may want to search on the Internet if you are interested in digging further on this topic, but Dr. Dave Clunie has written an excellent article here on why this may be the case.