DICOM Basics using .NET and C# - Query and Retrieve Operations (C-FIND)


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

Fellow Oak (fo-dicom) 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 fo-dicom DICOM Toolkit. This is a completely stand-alone DICOM toolkit that implements functionality such as DICOM file and directory processing as well DICOM networking-related operations. This toolkit is completely free for both commercial or non-profit use. 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 the Fellow Oak DICOM 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 Fellow Oak DICOM library, I would encourage you to visit its website itself or check out the fo-dicom issues pages for details.

“I let his voice be my comfort. It bore no trace of pain or self-pity, carrying only good humor and softness and just the tiniest hint of jazz. I lived on it as if it were oxygen. It was sustaining, and it was always enough.” ~ Michelle Obama (speaking of her father)

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 .NET development environment as well as the Fellow Oak (fo-dicom) DICOM library before you can run the example if you want to try this out yourself.

  • Download a .NET IDE or Editor such as Visual Studio IDE or Visual Studio Code (even a text editor should suffice)
  • Download the Fellow Oak DICOM library either through NuGet Package Manager or download the source code directly from here
  • You can also 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.

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 (using a 'C-CANCEL-FIND-RQ' command) 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" which can be 'Patient Root', 'Study Root' or 'Patient/Study Only'. Of these, the 'Study Root' is the most popular method used for query operations. The 'Patient/Study Only' level was retired sometime ago but you may find some old DICOM software that may still support it. 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. Something I should also mention here is that the C-FIND operation is used in both the DICOM Modality Worklist operation as well as the DICOM General Purpose Worklist operation (even if there are some differences in the information model for the data that is exchanged during these operations). I plan to cover both these operations in more detail a separate tutorial in the near future.

“Everyone who remembers his own education remembers teachers, not methods and techniques. The teacher is the heart of the educational system” ~ Sidney Hook

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). These are referred to as "Matching Keys" in DICOM. In addition, you also need to explicitly specify the attributes of data that you want returned when a match occurs (referred to as "Return Keys"). 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.

    using System;
    using System.Collections.Generic;
    using Dicom;
    using Dicom.Network;

    namespace DicomFindOperation
    {
        public class Program
        {
            // Dr. Harvey graciously provides a free DICOM server to connect and play with
            private static string QRServerHost = "www.dicomserver.co.uk";
            private static int QRServerPort = 104;
            private static string QRServerAET = "STORESCP";
            private static string AET = "FODICOMSCU";

            static void Main(string[] args)
            {
                //create a C FIND SCU Client that filters based on patient name 
                var client = CreateCFindScuDicomClient("Bowen*");
                
                client.Send(QRServerHost, QRServerPort, false, AET, QRServerAET);

                Console.ReadLine();
            }

            public static DicomClient CreateCFindScuDicomClient(string patientName)
            {
                var cFindScuDicomClient = new DicomClient();
                cFindScuDicomClient.NegotiateAsyncOps();

                var request = new DicomCFindRequest(DicomQueryRetrieveLevel.Study);

                // To retrieve the attributes of data you are interested in
                // that must be returned in the result
                // you must specify them in advance with empty parameters like shown below

                request.Dataset.AddOrUpdate(DicomTag.PatientName, "");
                request.Dataset.AddOrUpdate(DicomTag.PatientID, "");
                request.Dataset.AddOrUpdate(DicomTag.StudyDate, "");
                request.Dataset.AddOrUpdate(DicomTag.StudyInstanceUID, "");

                // Specify the patient name filter 
                request.Dataset.AddOrUpdate(DicomTag.PatientName, patientName);

                // Specify the encoding of the retrieved results
                // here the character set is 'Latin alphabet No. 1'
                request.Dataset.AddOrUpdate(new DicomTag(0x8, 0x5), "ISO_IR 100");

                // Find a list of Studies
                var studyUids = new List<string>();
                request.OnResponseReceived += (req, response) =>
                {
                    LogStudyResultsFoundToDebugConsole(response);
                    studyUids.Add(response.Dataset?.GetSingleValue<string>(DicomTag.StudyInstanceUID));
                };

                //add the request payload to the C FIND SCU Client
                cFindScuDicomClient.AddRequest(request);

                //Add a handler to be notified of any association rejections
                cFindScuDicomClient.AssociationRejected += OnAssociationRejected;

                //Add a handler to be notified of any association information on successful connections
                cFindScuDicomClient.AssociationAccepted += OnAssociationAccepted;

                //Add a handler to be notified when association is successfully released - this can be triggered by the remote peer as well
                cFindScuDicomClient.AssociationReleased += OnAssociationReleased;

                return cFindScuDicomClient;
            }

            public static void LogStudyResultsFoundToDebugConsole(DicomCFindResponse response)
            {
                //data will continue to come as long as the response is 'pending' 
                if (response.Status == DicomStatus.Pending)
                {
                    var patientName = response.Dataset.GetSingleValueOrDefault(DicomTag.PatientName, string.Empty);
                    var studyDate = response.Dataset.GetSingleValueOrDefault(DicomTag.StudyDate, new DateTime());
                    var studyUID = response.Dataset.GetSingleValueOrDefault(DicomTag.StudyInstanceUID, string.Empty);

                    LogToDebugConsole("Matched Result...");
                    LogToDebugConsole($"Patient Found ->  {patientName} ");
                    LogToDebugConsole($"Study Date ->  {studyDate} ");
                    LogToDebugConsole($"Study UID ->  {studyUID} ");
                    LogToDebugConsole("\n");
                }

                if (response.Status == DicomStatus.Success)
                {
                    LogToDebugConsole(response.Status.ToString());
                }
            }

            private static void OnAssociationAccepted(object sender, AssociationAcceptedEventArgs e)
            {
                LogToDebugConsole($"Association was accepted by:{e.Association.RemoteHost}");
            }

            private static void OnAssociationRejected(object sender, AssociationRejectedEventArgs e)
            {
                LogToDebugConsole($"Association was rejected. Rejected Reason:{e.Reason}");
            }

            private static void OnAssociationReleased(object sender, EventArgs e)
            {
                LogToDebugConsole("Association was released. BYE BYE");
            }

            private static void LogToDebugConsole(string informationToLog)
            {
                Console.WriteLine(informationToLog);
            }

        }
    }

“Don’t just teach your children to read. Teach them to question what they read. Teach them to question everything.” ~ George Carlin

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


Association was accepted by:www.dicomserver.co.uk
Matched Result...
Patient Found ->  Bowen^William^^Dr
Study Date ->  2019-10-17 12:00:00 AM
Study UID ->  1.2.826.0.1.3680043.11.106

Matched Result...
Patient Found ->  Bowen William  Dr
Study Date ->  2019-10-01 12:00:00 AM
Study UID ->  1.2.826.0.1.3680043.11.105

Matched Result...
Patient Found ->  Bowen William  Dr
Study Date ->  2020-04-03 12:00:00 AM
Study UID ->  1.2.826.0.1.3680043.11.104

Matched Result...
Patient Found ->  Bowen^William
Study Date ->  2020-04-20 12:00:00 AM
Study UID ->  2.25.277334392552870554060358972926138757790

Matched Result...
Patient Found ->  Bowen^William
Study Date ->  2020-04-20 12:00:00 AM
Study UID ->  2.25.311520545447777953290814166086852478277

Matched Result...
Patient Found ->  Bowen^William
Study Date ->  2020-04-20 12:00:00 AM
Study UID ->  2.25.338597757323940228728402761266247107967

Matched Result...
Patient Found ->  Bowen^William
Study Date ->  2020-04-20 12:00:00 AM
Study UID ->  2.25.70738700343845585472495779649571636344

Matched Result...
Patient Found ->  Bowen^William
Study Date ->  2020-05-12 12:00:00 AM
Study UID ->  1.2.826.0.1.3680043.9.6384.2.5000040.20200512135605.568.13

Success
Association was released. BYE BYE

Testing Tools for DICOM Query Operations

When you need to troubleshoot query operations in DICOM, it will helpful to use one of the many useful DICOM testing tools out there. The one that I have used in the past and have liked is DCMTK. The toolkit comes with many standalone testing utilities that help you test various aspects related to DICOM processing through a command line interface. The findscu command is the one that I will use here to retrieve matching study results matched for a specified patient from Dr. Dave Harvey's free online DICOM test server provided here. This server is listening to requests on port 104 in the example shown below. Here, I am specifying the query root as "STUDY", the patient name I am searching as "SAMUAL", and am also specifying the attributes of data that I would like to retrieve with the matched results (StudyInstanceUID and the StudyDate attributes here). Note:Only a portion of the console output generated when running this command is shown below for reference.


C:\SaravananDicomTestingTools\dcmtk-3.6.5-win64-dynamic\bin>findscu -v -S -k 0008,0052="STUDY" -k PatientName="SAMUAL*" -k StudyInstanceUID="" -k StudyDate="" -aet OurFindScu "www.dicomserver.co.uk" 104 -aec MEDCONNECTIONS

I: Requesting Association
I: Association Accepted (Max Send PDV: 65524)
I: Sending Find Request (MsgID 1)
I: Request Identifiers:
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0020) DA (no value available)                     #   0, 0 StudyDate
I: (0008,0052) CS [STUDY]                                  #   6, 1 QueryRetrieveLevel
I: (0010,0010) PN [SAMUAL*]                                #   8, 1 PatientName
I: (0020,000d) UI (no value available)                     #   0, 0 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 1 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378443245758100] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 2 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378443765491270] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 3 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378449008913780] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 4 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378449024588160] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 5 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378449030292020] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 6 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378449082833540] #  52, 1 StudyInstanceUID
I:
I: ---------------------------
I: Find Response: 7 (Pending)
I:
I: # Dicom-Data-Set
I: # Used TransferSyntax: Little Endian Explicit
I: (0008,0005) CS (no value available)                     #   0, 0 SpecificCharacterSet
I: (0008,0020) DA [20200318]                               #   8, 1 StudyDate
I: (0008,0052) CS [STUDY ]                                 #   6, 1 QueryRetrieveLevel
I: (0008,0054) AE [MEDCONNECTIONS]                         #  14, 1 RetrieveAETitle
I: (0010,0010) PN [Samual^Davies ]                         #  14, 1 PatientName
I: (0020,000d) UI [1.3.6.1.4.1.30071.8.247186163303517.6378449085807630] #  52, 1 StudyInstanceUID
I:
I: Received Final Find Response (Success)
I: Releasing Association
I:
I: Received Final Find Response (Success)
I: Releasing Association

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!