DICOM Query/Retrieve Part I


It all started when I was sitting in a cubicle with a customer, looking at the code of their workstation performing a Query/Retrieve cycle and though everything did look familiar and pretty much straight forward something bothered me.

Query/Retrieve, or Q/R for short, is the DICOM service for searching images on the PACS and getting a copy of them to the workstation where they can be displayed.

Q/R is a fundamental service and every workstation implements it. This sounds like a trivial task, just like downloading a zip file from a web site but there are a lot of details to take care of and while writing this post I realized that I will have to split it to a little sub-series. Today's post will be about the Query part and in the next post I'll get to the Retrieve.

To search the PACS we use the DICOM command C-FIND. This command takes as an argument a DICOM object that represent a query. The PACS transforms the object that we send to a query, probably to SQL, runs it and then transform every result record back into a DICOM object and send it back to us in a C-FIND response. The PACS sends one C-FIND response for every result record. While still running, the status field of the C-FIND response command is pending (0xFF00). The last response has a status success. It may of course fail and then RZDCX will throw an exception with the failure reason and status. It may also succeed but with no matches (empty results set).

Let's do some examples. This code constructs a query for searching patients:

            // Fill the query object
            DCXOBJ obj = new DCXOBJ();
            DCXELM  el = new DCXELM();

            el.Init((int)DICOM_TAGS_ENUM.QueryRetrieveLevel);
            el.Value = "PATIENT";
            obj.insertElement(el);

            el.Init(0x00100010);
            el.Value = "R*";
            obj.insertElement(el);

            el.Init(0x00100020);
            obj.insertElement(el);

            el.Init((int)DICOM_TAGS_ENUM.PatientsSex);
            obj.insertElement(el);

            el.Init((int)DICOM_TAGS_ENUM.PatientsBirthDate);
            obj.insertElement(el);

This code creates a DICOM object that the equivalent pseudo SQL of is:

SELECT [PATIENT NAME] , [PATIENT ID], [PATIENT SEX], [PATIENT BIRTH DATA]
FROM PATIENT
WHERE [PATIENT NAME] like "R%"

Here's a study level query with explanations:
// Fill the query object
DCXOBJ obj = new DCXOBJ();
DCXELM el = new DCXELM();


            el.Init((int)DICOM_TAGS_ENUM.studyInstanceUID);
            obj.insertElement(el);

SELECT
[STUDY INSTANCE UID],

el.Init((int)DICOM_TAGS_ENUM.StudyDate);
            obj.insertElement(el);
           
[STUDY DATE],

el.Init((int)DICOM_TAGS_ENUM.StudyDescription);
            obj.insertElement(el);
           

[STUDY DESCRIPTION],
el.Init((int)DICOM_TAGS_ENUM.ModalitiesInStudy);
            obj.insertElement(el);


[MODALITIES IN STUDY]
     el.Init((int)DICOM_TAGS_ENUM.QueryRetrieveLevel);
            el.Value = "STUDY";
            obj.insertElement(el);


FROM STUDY
            el.Init((int)DICOM_TAGS_ENUM.patientName);
            el.Value = "REIMOND^GOLDA";
            obj.insertElement(el);
WHERE
[PATIENT NAME] = ‘REIMOND^GOLDA’


el.Init((int)DICOM_TAGS_ENUM.patientID);
            el.Value = "123456789";
            obj.insertElement(el);

AND

[PATIENT ID] = ‘123456789’


This code creates a DICOM object that the equivalent pseudo SQL of is:

SELECT [PATIENT NAME] , [PATIENT ID], [STUDY INSTANCE UID], [STUDY DATE], [STUDY DESCRIPTION], [MODALITIES IN STUDY]
FROM STUDY
WHERE [PATIENT NAME] = ‘REIMOND^GOLDA’ AND [PATIENT ID] = ‘123456789’

The analogue to SQL is very simple. Here are the rules:
  1. The SELECT list, i.e. the list of ‘columns’ that we would like to get in the response is the list of all elements added to the query object.
  2. The FROM table is set in the element (0008,0052) – Query Retreive Level and can be one of the following coded strings: PATIENT, STUDY, SERIES or IMAGE
  3. The WHERE clause i.e. the elements that the matching is made with, is comprised of all the element that we set a value with a logical AND between them.
The WHERE clause can be further refined using some basic wildcard matching:
Wildcard
Meaning
SQL eqivalent/Meaning
*
Zero or more character in string values
WHERE PATIENT NAME LIKE “COHEN%”

?
Any Single character
“COH?N” will match “COHEN” and “COHAN” and also “COH N”
I don’t recommend using it.
-
For date and time attributes
FROM – TO in the following form
YYYYMMDD-YYYYMMDD
WHERE STUDY DATE BETWEEN 19950101 AND 20110911
YYYYMMDD-
WHERE STUDY DATE >= 19950101
-YYYYMMDD
WHERE STUDY DATE <= 20110911
\
Value list matching
LOCALIZER\AXIAL
WHERE IMAGE TYPE in (‘LOCALIZER’, ‘AXIAL’)

There's also sequence matching that I will not explain here. and multi-value matching using the \ separator that I don't encourage you to use because my experience shows that many PACS don't implement it the way you would expect. Nevertheless, when it does work, it can be very handy. My advise, when implementing a C-FIND SCU, i.e. a PACS search client, make it as configurable as possible and give your field engineer the power to enable advanced search options for specific sites or AE titles. By default however, stick to the basics and set all the advanced options off.

When combination of date and time elements are used, the standard states that the PACS should combine them according to what one would expect so for example this:

            el.Init((int)DICOM_TAGS_ENUM.StudyDate);
            el.Value = "19950101-20110911";
            obj.insertElement(el);
            el.Init((int)DICOM_TAGS_ENUM.StudyTime);
            el.Value = "090000-170000";
            obj.insertElement(el);

Should match all studies made between Jan. 1st 1995 at 9AM to Sep. 11 2011 at 5PM and not studies made from 9 to 5 in that date period. However I wouldn’t relay on that so don’t be surprised if some PACS will actually get it the second way because it was easier to implement. Open standard, did I say that already?

Continuing this line of thoughts I wouldn’t recommend having your DICOM Workstation relay on queries like this “COHEN*^A*M^MR” expecting the PACS to convert it to something like WHERE FIRST NAME LIKE ‘A%M’ AND LAST NAME LIKE ‘COHEN%’ and TITLE = ‘MR’. Stick to the basics instead.

What I would recommend is to have your workstation search screen simple as possible with the following attributes: Patient Name, Patient ID, Sex, Birth Date, Study Date and Accession Number.

If you can make the search configurable, make it as configurable as possible and also prepare configuration presets for specific PACS vendors. From personal 1st hand experience, it takes nothing more than an uncommon query attributes to crash a PACS. Try making a range matching on Body Part as a start.

Before moving forward, this is a good oportunity to explain the ^ in DOE^JOHN. Elements of type PN (VR - Value Representation, remember) i.e. that their value represent a Person Name follow a notation shared by DICOM and HL7. The ^ is called component separator and it separates the elements of the Person Name which are by order: Family Name^Given Name^Middle Name^Prefix^Suffix. Here are some examples:
  • Usain Bolt would be “BOLT^USAIN” 
  • President Barak Hussein Obama II would be “Obama^Barak^Hussein^President^II” 
BTW, the PACS should do the string matching ignoring the case (case insensitive). Don't be surprised if you bump into one that is case sensitive.

The query retrieve level tag (0008,0052) sets the 'table' we are selecting from. You can imagine the PACS having a database with the following hierarchical data model that we've already seen when talking about DICOM objects.
Query Levels
A good question is which are the primary keys of each level and what columns are there. Again, this is very much implementation specific and the best way to check is to read the DICOM conformance statement but the standard does provide definitions for that but, another but, it provides 3 different definitions!?:((( Don't ask why

Originally, the DICOM standard defined three data models for the Query/Retrieve service. each data model has been assigned with one UID for the C-FIND, one UID for the C-MOVE and one UID for C-GET so all together there were 9 UIDs, 3 for search (C-FIND), 3 for download (C-MOVE) and 3 for the sync download (C-GET). C-GET got obsolete so 3 went down and then one data model called "Patient/Study Only" got obsolete too so we are now left with 'only' 4 but then some more models were added and C-GET also got back to life. Confused? don't worry, the best advise I can give you is this: Just use Patient Root  because everyone supports it (I know IHE recommends Study Root). 

Here are the models with their valid query levels

UID
Name
Query Levels
Comment
1.2.840.10008.5.1.4.1.2.1.1
Patient Root Query/Retrieve Information Model - FIND
PATIENT
STUDY
SERIES
IMAGE
Use it!
1.2.840.10008.5.1.4.1.2.1.2
Patient Root Query/Retrieve Information Model - MOVE
PATIENT
STUDY
SERIES
IMAGE
Use it!
1.2.840.10008.5.1.4.1.2.2.1
Study Root Query/Retrieve Information Model - FIND
STUDY
SERIES
IMAGE
Use it if Patient root doesn’t work for you
1.2.840.10008.5.1.4.1.2.2.2
Study Root Query/Retrieve Information Model - MOVE
STUDY
SERIES
IMAGE
Use it if Patient root doesn’t work for you
1.2.840.10008.5.1.4.1.2.3.1
Patient/Study Only Query/Retrieve Information Model - FIND (Retired)
PATIENT
STUDY
Don’t use
1.2.840.10008.5.1.4.1.2.3.2
Patient/Study Only Query/Retrieve Information Model - MOVE (Retired)
PATIENT
STUDY
Don’t use

What are the primary keys for each level? For patient level the key is patient id but I recommend that you always use it with combination with patient name. For all the other levels it's the UID of that level (Study Instance UID for Study, Series Instance UID for Series, SOP Instance UID for IMAGE). For Study level you can safely use Accession Number as a search key. Everyone supports it too. If you can stop at the study level, that's the best. Just download complete studies. If you have to drill down to series and image level, just don't make expectations about what the PACS is going to send you back. Each one has it's own flavor and set of supported elements. Don't get me wrong though, most PACS behave nicely but every now and then you just trip on some home brow PACS that makes you sweet. 

The default implementations don't support relational queries so in order to find all the SOP Instance UID's you should first make a Study Level Query to get the Study Instance UID, then use it in a series level query to get the list of Series Instance UID's then query once for every Series Instance UID at Image level to get all the SOP instances of that series, then combine them all.

There are also counters at each level that you can use to get the number of child records:
(0020,1200)
Number of Patient Related Studies
(0020,1202)
Number of Patient Related Series
(0020,1204)
Number of Patient Related Instances
(0020,1206)
Number of Study Related Series
(0020,1208)
Number of Study Related Instances
(0020,1209)
Number of Series Related Instances

I'm not going to list here all the mandatory and optional attributes at each level. You can check this in part 4 of the standard (section C.6). Just remember that all the elements that are marked with O are optional so not all PACS will support them. In you implementation make sure to be tolerant for not having them and relay only on the required fields.

It's time to run the query and get the results. With RZDCX you have two ways of doing it. The easy way is simply to iterate over the return value of Query. The Query methods of DCXREQ returns an Object Iterator DCXOBJIterator and you can iterate over the results like this:

// Create the requester object
DCXREQ req = new DCXREQ();
// send the query command
it = req.Query(LocalAEEdit.Text,
               TargetAEEdit.Text,
               HostEdit.Text,
               ushort.Parse(PortEdit.Text),
               "1.2.840.10008.5.1.4.1.2.1.1",
               obj);
DCXOBJ currObj = null;
try
{
    // Iterate over the query results
    for (; !it.AtEnd(); it.Next())
    {
        currObj = it.Get();
        string message = "";
        DCXELM currElem = currObj.getElementByTag(0x00100020);
        if (currElem != null)
        {
            message += "" + currElem.Value;
        }
        currElem = currObj.getElementByTag(0x00100010);
        if (currElem != null)
        {
            message += " " + currElem.Value;
        }
        // …
    }
}
catch (Exception ex)
{
    MessageBox.Show("ex.Message);
}

The advantage of the code above is that it’s very simple but you have to wait until the query ends to show the results. Another way is to add a callback like this:

DCXREQ req = new DCXREQClass();
req.OnQueryResponseRecieved += new IDCXREQEvents_OnQueryResponseRecievedEventHandler(QueryCallback);

And then send the query as before. The callback looks something like this:

public void QueryCallback(DCXOBJ obj)
{
    DCXELM e = obj.getElementByTag((int)DICOM_TAGS_ENUM.patientID);
    // ... fill the results grid
}

In this callback you can fill the results grid, update the progress button and more important stop the query if you get more results than you expected simply by throwing an application exception like the commented line in the callback above. The query response is called by RZDCX once for every C-FIND response that the called AE sends.

Lets summarize because this was a long long post:
  1. A DICOM Query is represented using a DICOM Object
  2. Empty elements are like the select list
  3. Elements with value are like the where 
  4. Between each element there's a logical AND
  5. You can use * for string matching and - for date and time ranges
  6. The Query Level tag (0008,0052) is the FROM table
  7. The data model has 4 hierarchical levels PATIENT - STUDY - SERIES - IMAGE
  8. Build your Query client as tolerant as possible.
    • Do not put hard constraints if some elements are missing
    • Use the results for display to the user and if something is missing, mark it clear in the UI
  9. Do not overload functionality on the Query flow. 
    • Run the query, display progress bar, at the most update the results grid dynamically
    • After the query ends, over the data and see what you've got and if you can go along with it.
Check the example QueryRetrieveSCUExample on RZDCX download page.
In the next post I'll explain the retrieve part of the Q/R Service.
As always, comments and questions are most welcome.

15 comments:

  1. Please provide the mandatory attribute for each level in at-least one root (Patient/Study). In Conformance statement its too abstract.

    ReplyDelete
  2. Hi Vinod,
    You can find the standard definition on part 3 of the standard, section C.6
    For Patient Root, patient level patient name is 'R' (Required) and patient ID is 'U' (required and the unique key) though the patient ID is not always unique.
    For study level in Patient root, the Study Instance UID is 'U' and Study Date, Study Time, Accession Number and Study ID are 'R'. All others are 'O'.

    ReplyDelete
  3. Hi Roni, amazing blog. My compliments for you.
    I'm using PACS and I tried to get Patient, Study and Series using the DCXOBJ and DCXREQ. It works fine. Including using conditions such as Patient Name, Patient ID, Study Date, etc. I succeeded in getting data in all QueryRetrieveLevel.
    How do I get the image data? Any idea? I want to show the images from Series but I'm not succeeding. I tried to add PixelData to query (el.Init((int)DICOM_TAGS_ENUM.PixelData); obj.insertElement(el);) but it doesn't work. When I execute "req.Query(...)" I receive an exception: "0006:020e DIMSE Failed to send message - Normal". Could you help me?

    Thanks in advance,
    Andre Belloti

    ReplyDelete
    Replies
    1. Hi Andre,
      I'm glad you like it. Thanks!
      To get the images you should use the C-MOVE command.
      Look at the next post (Q/R Part 2) http://dicomiseasy.blogspot.co.il/2012/02/c-move.html
      For an example
      Regards,
      Roni

      Delete
  4. Hi Roni,

    Thanks for all the tutorials.
    Currently I managed to get all "primary keys" from every hierarchy level and now I am having trouble querying the SOPClassUID (which should be in SOP Common module of Image level) and FrameOfReferenceUID (which is not part of any level according to the A.17.3 RT Image IOD Module Table in documentation part 3.).

    Can you please help me with this problem?

    Regards,
    Igor

    ReplyDelete
    Replies
    1. Hi Igor,
      SOP Class UID is an optional key in Patient root Q/R Model C-FIND IMAGE level. See (2009) P.3-4 C.6.1.1.5.
      Frame of reference UID is optional too.
      It important to check the capabilities of the specific PACS you work with. You can check this in the conformance statement.
      As a good practice of application development I suggest not to relay on any optional keys as well as to have a fallback for cases that required keys are not behaving as expected too.
      Roni

      Delete
  5. I have been hurt by the relationship of the ROOT and the LEVEL for a long long time.It's very useful for me~ Thank you!

    ReplyDelete
  6. Hi,

    Please explain about Patient root, study root and patient study only query models and the difference among those.

    Thanks in advance.

    ReplyDelete
  7. How to send .dcm image through Query SCP emulator to Synedra. To do the query retrive testing. It showing,
    "Error while performing DICOM CFIND request".
    How can I overcome this problem.

    ReplyDelete
    Replies
    1. Not enough information to answer this, sorry. Send using Query SCP? Synedra? Error while perfuming ...? what error? Can you get more information? One thing I can say about DICOM software that they lack good error handling and diagnostic information. Can't help here. Sorry.

      Delete
  8. How would you limit the amount of results returned. So lets say I have a data base of 100,000 images. I want to search for images taken on a certain day but because I don't want to kill the network I want to limit returned results of only 100.

    ReplyDelete
    Replies
    1. Hi Michael
      With RZDCX toolkit, you can return a E_FAIL HRESULT (or throw exception in C#) from the OnQueryResponseReceived event (http://rzdcx.roniza.com/group___q.html#gaffff639b5ce0133105dc8fb7c23341ed)
      This will send a C-CANCEL command to the SCP that should stop the query.
      You count of course on the SCP that implemented the C-CANCEL and will indeed stop sending results.
      If the SCP doesn't stop, you must hard stop the query somehow from your side.
      In RZDCX your E_FAIL will do this.
      I try not to send queries that may result with many responses. Try for example querying in small date ranges or even with time combinations

      Delete
  9. Is it possible to query for multiple values for a particular tag? For e.g. retrieve all images belong "patient_1" AND "patient_2" in a single command..

    ReplyDelete
    Replies
    1. Great question. DICOM has multi value query syntax using the \ separator so you may try using patient name = a\b but this is not at all common and I wouldn't relay on that to work when designing a system. Instead I would recommend sending two queries for a and b and unifing the results

      Delete