Link

11 Jun 2016

DICOM Basics using Java - Query and Retrieve Operations (C-FIND)

Contents

  1. Introduction
  2. The PixelMed Java DICOM Toolkit - Quick Overview
  3. Before We Get Started…
  4. PACS Server Requirement
  5. Querying DICOM Data using C-FIND
  6. Example of C-FIND operation
  7. Conclusion

How the C-FIND 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 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, we are going to begin our exploration around the aspects of DICOM networking that deal with search and retrieval of artifacts such as images and structured reports that are encoded in DICOM format. In this article, we are going to explore how to build a DICOM client (a 'C-FIND SCU' in DICOM terminology) that can query a remote peer (called a 'C-FIND SCP') for information one may be interested in. In the next few articles, I will gradually build on the foundation laid in this article and show you how to retrieve these artifacts for review locally (using 'C-MOVE SCU' and 'C-GET-SCU' DICOM operations). These operations are called Composite Services in DICOM (see my tutorial on DICOM Associations) for more information).

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.

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

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 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)

  4. You can also find the source code used in this tutorial on GitHub

  5. I have included some sample DICOM images along with the source code to follow along. However, please feel free to use your own images as required

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 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 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.

Querying DICOM Data using C-FIND

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, several activities happen. The two devices first check to see if they are accesible and can actually 'speak' DICOM (done through “DICOM ping” which is more formally known as “C-Echo” which we saw in an earlier tutorial). They also check to see if they are allowed to communicate with other from a DICOM security standpoint. This is done by checking whether the DICOM AE configurations have been set up on both sides. They then check to see if they support the DICOM operation that is desired often referred to as the "abstract syntax". Then, as a last step, they must check to see if they can agree on a "transfer syntax" for the information being exchanged (such as the VR encoding, compression and the byte ordering to be used). All these steps have to be completed before they proceed to performing the actual operations that are desired.

During the operational execution phase, the two DICOM peers exchange something called DICOM Message Service Elements (DIMSEs) with one another. These objects help indicate the actual operation that is required to be peformed, and they are accompanied by additional data referred to as Information Object Definitions (IODs) such as textual information or images that these operations are performed on. Together, these service elements and information objects they act on combine to form what are referred to in DICOM as Service Object Pairs (SOPs). The type of DIMSEs exchanged vary according to the type of operation being performed. For DICOM query operations, a C-FIND-RQ message is passed from the querying device (Device A) to the C-Find SCP (Device B) as shown in the illustration below. The C-FIND-RQ request message is accompanied by an IOD that consists of both the search criteria and also the attributes of data that need to be returned if matches are found.

Query Retrieve Sequence Diagram

The structure of the C-FIND-RQ message request and message response objects is shown below. The tables shown are screen captures from the DICOM standard part 7 document that covers message exchange fundamentals. The queried SCP returns a C-FIND-RSP response message for each entity matching the identifier specified in the request. Please note that the responses themselves are returned as DIMSE-IOD pairs with the DIMSE object communicating the status of the resposne operation (such as 'pending' when they are more results to follow in the sequence and 'successful' when all data has all been transmitted). The remote party may also transmit other DIMSE command responses for situations such as when there are no matching results or if other errors occur during the search operation. The client may also initiate a cancel operation anytime while the search operation is in progress at which time the C-FIND-SCP will then cancel its search operation and return a DIMSE command with a status of 'cancelled'. Please see the official DICOM documentation for more details as the specification is huge and I can only provide an overview in this article.

DICOM Find Request and Response

For all query and retrieval services (also called 'Composite Services') in DICOM, data is looked up using something known as a "query level" (can be 'Patient Root', 'Study Root' and 'Patient/Study Only'). Along with a query level, a matching set of filters that are pertinent to the chosen query level are transmitted. The remote peer that receives the query request uses these filters essentially like a SQL search wildcard expression matching it to the relevant DICOM files that it may contain. Please note that official DICOM does not permit relational database type queries. However, some vendors choose to implement additional capabilities that are "SQL-like" into their products that operate on top of the core DICOM query features to make them behave very similar to any SQL tools that end users may already be familiar with. The DICOM standard itself specifies many kinds of search filtering through six kinds of matchers. Please see official DICOM documentation on search attributes and matching for more information.

“You have no responsibility to live up to what other people think you ought to accomplish. I have no responsibility to be like they expect me to be. It’s their mistake, not my failing” ~ Richard P. Feynman

Example of C-FIND operation

Let us have quick look at some code now. As mentioned earlier, the C-FIND operation uses a hierarchial data model during searches. The "query levels" specified in DICOM are 'Patient Root', 'Study Root' and 'Patient/Study Only'. However, it is also important to be aware of information mode hierarchy as well (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 (when using 'Study Root' query level). In addition, you also need to explicitly specify the attributes of data that you want returned when a match occurs. There is lot more to search, but let us take a look at a quick example using the PixelMed DICOM toolkit to perform a 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 commercial solution for your DICOM requirement that is compatible with the Microsoft platform.

    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'
                //the API construct is a bit different in PixelMed for those you are used to invoking a 'command' method
                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);
            }
        }

    }

“The greatest use of a life is to spend it for something that will outlast it” ~ William James

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 (I will let you explore this on your own).


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>

Conclusion

This concludes the article on how the C-FIND query operation work in DICOM. This enables the first step in the overall process in performing most information retrieval operations in DICOM. In the next tutorial in this series on the DICOM standard, I will cover how another composite operation called "C-MOVE" which helps instruct a remote DICOM server (a 'C-MOVE SCP') to transfer DICOM data back to the client or to a specified location. See you then!