DICOM Basics - Query and Retrieve Operations - Part 1 of 2

This 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 might be useful to also 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. My DICOM networking-related tutorials covering my series of articles on the DICOM standard including DICOM Verification as well as DICOM Associations will also be very useful to understanding the material that I will cover in this tutorial. This tutorial also assumes that you know the basics of Java or any equivalent object-oriented language such as C# or C++. A basic understanding of networking will also be useful to have but is not mandatory.

Introduction

In this tutorial, I am going to provide a quick introduction to an extremely useful DICOM service that is used as part of clinical workflows around the world. It is called the DICOM Query/Retrieve service. As this topic is pretty big, I will break it down into two parts covering the basics here, and will go into it in depth in a future article. The query/retrieve service is often used to query a DICOM device such as a PACS server or another DICOM workstation and later retrieve any results that matched the search criteria. The interesting thing to note is that the results can not only retrieved back to the querying device but also to a completely different DICOM device.

The query/retrieve service is made up of two separate phases namely the Query Phase as well as a Retrieve Phase. During the query phase, the Calling AE or SCU queries another DICOM device (the Called AE or the SCP) for content it is interested in passing in any search criteria such as patient, study or series information. The SCP then responds back with the results. At this stage, the user may or may not decide to retrieve these results to his/her workstation. If the user does choose to retrieve the results matched by the query criteria, then another distinct operation is invoked where the results are pushed back to the querying device through the use of the DICOM Storage service. Something very interesting occurs here. During this operation, the queried device or the SCP now switches roles to become a Storage SCU and pushes the results to the querying device which now plays the role of the Storage SCP. Don’t worry too much if this does not make sense to you as we will cover this in detail as we progress through this tutorial.

“The highest education is that which does not merely give us information but makes our life in harmony with all existence.” ~ Rabindranath Tagore

In one of my earlier tutorials, you learnt about the four level hierarchy used in DICOM consisting of Patient, Study, Series and Image (or Instance). You also learnt that the standard does not permit relational database type queries (and that this is only an optional feature that some applications implement on their own). The standard implements the query and retrieval of DICOM data using this four level information hierarchy as well. Let us look at this whole operation at a more detailed level covering any pre-requisite material we need to understand as we proceed.

DIMSEs in Query/Retrieve Operations

If you recall my tutorial on DICOM associations, you will recall that before any two DICOM devices can exchange service requests and results between each other, an association first needs to be established. During the association establishment/negotiation, the two devices may check to see if they speak DICOM (done through “DICOM ping” or “C-Echo” we saw earlier). whether they are authorized to communicate with each other (security check), whether they support the DICOM SOP class that is involved (C-Find, C-Store, etc), and lastly whether they agree on a transfer syntax that they can both understand. Only when all this is completed, do they proceed to transmit other commands pertaining to the actual service operation that is desired.

Query Retrieve Sequence Diagram

During the query phase, a C-Find message is passed from the querying device (here it is Device A) to the C-Find SCP (here it is Device B). The C-Find request message is accompanied by an IOD that consists of the search criteria. The structure of the C-Find message request and message response objects are shown below. The tables shown in the illustration below are screen captures from the DICOM standard part 7 document that covers message exchange fundamentals. The queried SCP returns a C-Find Response message for each entity matching the identifier specified in the request. Then, a final response message is sent indicating that the matching process is complete.

DICOM Find Request and Response

C-Find - A Quick Demo and First Look using PixelMed Toolkit

Enough theory. Let us have quick look at some code now. Just keep in mind that the C-FIND operation has a hierarchial data model that is utilized during searches. The structure of hierarchy (from top to bottom) is as follows: "PATIENT -> STUDY -> SERIES -> IMAGE/INSTANCE". Knowing this hierarchy is important to perform searches in DICOM systems. For instance, one needs to specify the attributes for the patient when searching for a study. Similary, searching for an series requires the specification of both the patient as well as study-related attributes. There is lot more to search, but let us take a look at a quick example using the PixelMed toolkit to perform the C-Find operation. I am going to connect to the public DICOM server made available by MedicalConnections information on which is provided here. Dr. Dave Harvey is a radiologist by background who runs this 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 solution for your DICOM requirement that is compatible with the Microsoft platform.

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 a production setting.

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.

  1. Download and install the Eclipse Java IDE from here (or use any other IDE you prefer)

  2. Download the PixelMed toolkit library from here

  3. Ensure that the PixelMed.jar library are 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)

  4. You can find the source code and the jpeg image used in this tutorial on GitHub

    package com.saravanansubramanian.dicom.pixelmedtutorial;

    import com.pixelmed.dicom.AttributeList;
    import com.pixelmed.dicom.SOPClass;
    import com.pixelmed.dicom.SpecificCharacterSet;
    import com.pixelmed.dicom.TagFromName;
    import com.pixelmed.network.FindSOPClassSCU;
    import com.pixelmed.network.IdentifierHandler;

    public class DicomCFindFunctionalityDemo {
        
        public static void main(String arg[]) {
            
            try {
                SpecificCharacterSet specificCharacterSet = new SpecificCharacterSet((String[])null);
                AttributeList identifier = new AttributeList();
                
                //specify attributes to retrieve and pass in any search criteria
                //query root of "study" to retrieve studies

                identifier.putNewAttribute(TagFromName.QueryRetrieveLevel).addValue("STUDY"); 
                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);
                
                //retrieve all studies belonging to patient with name 'Bowen'
                new FindSOPClassSCU("www.dicomserver.co.uk",
                        104,
                        "MEDCONNEC",
                        "MYJAVACLIENT",
                        SOPClass.StudyRootQueryRetrieveInformationModelFind,identifier,
                        new IdentifierHandler(),
                        3);
                
            }
            catch (Exception e) {
                e.printStackTrace(System.err); //in real life, do something about this exception
                System.exit(0);
            }
        }

    }

Results of running the code example is shown below. All studies for the patient with the name "Bowen" are returned with attributes that we specified. Using these results, one can then further narrow down on a specific study and the series of images associated with it if you needed to. There are many other of flavors of query mechanisms implemented in DICOM applications around the world with some implementations providing a SQL-type searches to enable searches to be similar to what we are accustomed to when searching relational database-backed applications. How they are implemented is beyond the scope of this article.

IdentifierHandler.doSomethingWithIdentifier():
(0x0008,0x0005) SpecificCharacterSet VR=<CS> VL=<0x0> <>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x0> <>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x6> <STUDY >
(0x0008,0x0054) RetrieveAETitle VR=<AE> VL=<0xa> <MEDCONNEC >
(0x0008,0x1030) StudyDescription VR=<LO> VL=<0x10> <CT Left Shoulder>
(0x0010,0x0010) PatientName VR=<PN> VL=<0xe> <Bowen^William >
(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.105>
IdentifierHandler.doSomethingWithIdentifier():
(0x0008,0x0005) SpecificCharacterSet VR=<CS> VL=<0x0> <>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x0> <>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x6> <STUDY >
(0x0008,0x0054) RetrieveAETitle VR=<AE> VL=<0xa> <MEDCONNEC >
(0x0008,0x1030) StudyDescription VR=<LO> VL=<0x10> <CT Left Shoulder>
(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.105>
IdentifierHandler.doSomethingWithIdentifier():
(0x0008,0x0005) SpecificCharacterSet VR=<CS> VL=<0xa> <ISO_IR 100>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x8> <20180306>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x6> <STUDY >
(0x0008,0x0054) RetrieveAETitle VR=<AE> VL=<0xa> <MEDCONNEC >
(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>
IdentifierHandler.doSomethingWithIdentifier():
(0x0008,0x0005) SpecificCharacterSet VR=<CS> VL=<0xa> <ISO_IR 192>
(0x0008,0x0020) StudyDate VR=<DA> VL=<0x8> <20180510>
(0x0008,0x0052) QueryRetrieveLevel VR=<CS> VL=<0x6> <STUDY >
(0x0008,0x0054) RetrieveAETitle VR=<AE> VL=<0xa> <MEDCONNEC >
(0x0008,0x1030) StudyDescription VR=<LO> VL=<0x0> <>
(0x0010,0x0010) PatientName VR=<PN> VL=<0xe> <Bowen^William >
(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.104>

In the next article in my series of articles on the DICOM standard let us look at a standalone DICOM server that will be useful for us in real-life situations and will be of help in some of the upcoming tutorials including Part 2 of this tutorial in this series when we look at DICOM query and retrieve operation in more depth.