Thursday, February 2, 2012

Query/Retrieve part II - C-MOVE


In part I of this post, I was in a meeting with a customer reviewing their workstation code and while sitting there I was thinking to myself, why should my customers have to deal with so many details of the DICOM Q/R Service when all they really want is to retrieve a study just like they would have downloaded a zip file from a web site. And thus, later, back in my office I decided to extended the DICOM Toolkit API to include a C-MOVE method that will take care of everything including the incoming association. In today’s post I’m going to use the new MoveAndStore method to talk about the DICOM Query/Retrieve service. We’ll start at the end and then work our way backwards.

C-MOVE is a DICOM command that means this: The calling AE (we) ask the called AE (the PACS) to send all the DICOM Instances that match the identifier to the target AE. 
Here’s how you ask a PACS to send you the DICOM images with RZDCX (version 2.0.1.9).

        public void MoveAndStore()
        {
            // Create an object with the query matching criteria (Identifier)
            DCXOBJ query = new DCXOBJ();
            DCXELM e = new DCXELM();
            e.Init((int)DICOM_TAGS_ENUM.patientName);
            e.Value = DOE^JOHN";
            query.insertElement(e);
            e.Init((int)DICOM_TAGS_ENUM.patientID);
            e.Value = @"123456789";
query.insertElement(e);
            // Create an accepter to handle the incomming association
DCXACC accepter = new DCXACC();
            accepter.StoreDirectory = @".\MoveAndStore";
Directory.CreateDirectory(accepter.StoreDirectory);
            // Create a requester and run the query
DCXREQ requester = new DCXREQ();
            requester.MoveAndStore(
                MyAETitle, // The AE title that issue the C-MOVE
                IS_AE,     // The PACS AE title
                IS_Host,   // The PACS IP address
                IS_port,   // The PACS listener port
                MyAETitle, // The AE title to send the
                query,     // The matching criteria
                104,       // The port to receive the results
                accepter); // The accepter to handle the results
        }

Behind this rather short function hides a lot of DICOM networking and when it returns we should have all the matching objects stored in the directory “.\MoveAndStore”. Readers with some practical DICOM experience probably expect me to say that it can also fail. In that case MoveAndStore throws an exception with the error code and description. Sometimes you would have to set the detailed logging on and start reading logs like we did in chapter 5 of this tutorial on DICOM networking and in some later post we will look together at a DICOM log of a Q/R transaction.

The following diagram, taken from part 2 of the DICOM standard, is commonly seen in DICOM Conformance Statements as the Data Flow diagram of the Q/R Service. These diagrams and their notation are defined by the standard in part 2 that specify the DICOM Conformance Statement – a standard document that every application vendor should provide and that describes how they implemented the standard in their product. At some point we will get to how to read and write these documents.




The vertical dashed line represents the DICOM Protocol Interface between the two applications (it is usually a single dashed line but in this example it got a bit messed up). The arrows accros the interface represents DICOM associations. The arrow points from the application that initiates the association (the requester) to the application that responds to it (the responder or accepter). The upper part of the diagram shows the control chanel where the C-MOVE request is sent and statuses are reported back by the PACS. The lower part of the diagram shows the data chanel where the DICOM instances are sent to the client.

There's a lot of activity behind the scenes of this method:
  1. The calling AE opens a network connection to the PACS and sends an association request with a Q/R C-MOVE presentation context. This association is like a control chanel of  the operation.
  2. The called AE (The PACS) examines the request and (hopefully) accepts the association and the Q/R C-MOVE presentation context and sends back an association accept primitive.
  3. The calling AE sends a C-MOVE command with the identifier (the content of the query variable of our function) as a parameter. The C-MOVE command also includes the target AE Title. 
  4. The PACS searches its internal configuration for the target AE Title. This AE must have been previously configured by the PACS administrator because the PACS must resolve the AE Title to IP address and port number of the target AE in order to initiates an association with it.
  5. The PACS transforms the identifier into a database query, runs the query on its internal database and compose a list with the matching DICOM instances.
  6. The PACS starts a new association to the target AE requesting the presenation contexts of the objects it intends to send. This association is like a data chanel of the operation.
  7. The PACS sends the matching instances using C-STORE commands, one C-STORE command for every matching DICOM instance.
  8. While sending the C-STORE commands on the second association (the data chanel started at step 6) the PACS may send status notifications in the form of C-MOVE responses on the first association (the control chanel started at step 1) with a pending status (0xFF00) and counters of how many instances were already sent and how many are there in total.
  9. After sending all the instances  the PACS closes the second association and sends a C-MOVE response with status success (or failure if something went wrong) and the C-MOVE command ends.
  10. The calling AE can close the association or send another command.
Many times the target AE is the same as the calling AE (we) so we ask the PACS to send the results to us.
The PACS (the called AE) who is the responder is acting as the SCP – Service Class Provider, the terms used in DICOM for a server. We (the calling AE) are the requester and are acting as the SCU – Service class user, the term used in DICOM for a client.

The target AE is acting as SCP for the C-STORE commands that the PACS sends. The new MoveAndStore method is intended solely for retreiving instances. To serve unsolisited storage commands, sometimes called DICOM push, we will use the Accepter class DCXACC that implements a DICOM server. We will see this later.

In the first part of the example above we've created a Query object. The rules for this object are almost identical to the ones we've already seen in part I when we've discussed the C-FIND command. The only difference is that we don't add empty elements because the results are DICOM instances sent to us and not records like in C-FIND.

Here are some things to remember and mistakes to avoid:
  • The pending C-MOVE responses are optional. The C-MOVE SCP may send pending responses while the transaction is preformed. Remember the may and don't count on these callbacks for anything important, i.e. not more then progress bar and status updates.
  • Some PACS will send a pending status after every instance, some will send one every 5 or 10 instances and some will send none.
  • Some PACS sends a success response immediately and only then start another associatin and send the resulting instances. This is not a valid implementation of DICOM but you may have to handle it. 
  • Isolate your DICOM implementation from your application. This is true for every software. Don't mix events from the 

It would have been nice to have a progress bar for the Retrieve action as well, right? Let’s add an event handler to the requester whenever a C-MOVE response is received. Here’s how:
The new release of RZDCX has an extended C-MOVE callback that was missing in previous releases. Adding this callback without breaking backwards compatibility is worth a post of its own. Versions prior to 2.0.1.9 have a Boolean parameter that is true as long as the command is going on. The new build (2.0.1.9) reports the command status and the four counters for completed, remaining, failed and warning sub-operations. Sub operations are the C-STORE commands on the data channel. With this callback adding a progress bar is quite easy:



req = new DCXREQ();
req.OnMoveResponseRecievedEx += new IDCXREQEvents_OnMoveResponseRecievedExEventHandler(MoveCallback);
// and now call MoveAndStore just the same

The implementation of MoveCallback should look like this:


void req_OnMoveResponseRecievedEx(
    ushort status,
    ushort remaining,
    ushort completed,
    ushort failed,
    ushort warning)
{
   // Update the progress bar and nothing more!
   // Throw an exception to cancel 
}


The callback is fired for every C-MOVE response with pending status that is sent by the SCP and again I remind you that the SCP may, meaning can but don’t have to, send pending C-MOVE responses. Some PACS will send one pending message for every C-STORE they make, others may send one every now and then and other PACS may not send pending messages at all. This means that you better avoid having important functionality coded or dependent somehow on this callback. I wouldn’t recommend anything more than a progress bar and would also have it clearly stated in the user manual that the progress bar behavior is at to the mercy of the PACS.

Sometimes you may wish to cancel the retrieve maybe because you get to many results or just the user clicked the cancel button. To do this, throw an exception (in C++ you can also return a failed HRESULT) in the callback. The toolkit will send a C-CANCEL command to the SCP on the control channel and the SCP should (hopefully) stop sending transaction.

That's it for today. In the next post we will improve our implementation by adding more event handlers and then split the Accepter from the Requester and handle the inbound association on a separate thread. This will  allow us to serve all incoming associations in the same manner. 

As always, questions and comments are most welcome.

20 comments:

  1. I have a question. You said it might now work at for me it didn't unfortunately.

    Here's my log

    2012-11-2917:09:55.178000 7728 INFO Association Request Parameteres:
    Our Implementation Class UID: 2.16.124.113543.6021.1
    Our Implementation Version Name: RZDCX_2_0_2_7
    Their Implementation Class UID:
    Their Implementation Version Name:
    Application Context Name: 1.2.840.10008.3.1.1.1
    Calling Application Name: DREGO
    Called Application Name: PACAE
    Responding Application Name: resp AP Title
    Our Max PDU Receive Size: 32768
    Their Max PDU Receive Size: 0
    Presentation Contexts:
    Context ID: 1 (Proposed)
    Abstract Syntax: =VerificationSOPClass
    Proposed SCP/SCU Role: Default
    Accepted SCP/SCU Role: Default
    Proposed Transfer Syntax(es):
    =LittleEndianExplicit
    =BigEndianExplicit
    =LittleEndianImplicit
    Context ID: 3 (Proposed)
    Abstract Syntax: =MOVEStudyRootQueryRetrieveInformationModel
    Proposed SCP/SCU Role: Default
    Accepted SCP/SCU Role: Default
    Proposed Transfer Syntax(es):
    =LittleEndianImplicit
    =LittleEndianExplicit
    =BigEndianExplicit
    Requested Extended Negotiation: none
    Accepted Extended Negotiation: none

    I'm assuming this means that I sent the request with no problem on my end.

    2012-11-2917:09:55.194000 7728 INFO Association Request Result: Normal
    Association Response Parameteres:
    Our Implementation Class UID: 2.16.124.113543.6021.1
    Our Implementation Version Name: RZDCX_2_0_2_7
    Their Implementation Class UID: 1.2.124.113532.3510
    Their Implementation Version Name: MITRAJUNE1997
    Application Context Name: 1.2.840.10008.3.1.1.1
    Calling Application Name: DREGO
    Called Application Name: PACAE
    Responding Application Name: PACAE
    Our Max PDU Receive Size: 32768
    Their Max PDU Receive Size: 100000
    Presentation Contexts:
    Context ID: 1 (Accepted)
    Abstract Syntax: =VerificationSOPClass
    Proposed SCP/SCU Role: Default
    Accepted SCP/SCU Role: Default
    Accepted Transfer Syntax: =LittleEndianImplicit
    Context ID: 3 (Accepted)
    Abstract Syntax: =MOVEStudyRootQueryRetrieveInformationModel
    Proposed SCP/SCU Role: Default
    Accepted SCP/SCU Role: Default
    Accepted Transfer Syntax: =LittleEndianImplicit
    Requested Extended Negotiation: none
    Accepted Extended Negotiation: none

    To me, according to your tutorial 3 I believe this means the connection was accepted and the command on 3 was also.

    2012-11-2917:09:55.194000 7728 INFO
    C-MOVE Identifier:
    # Dicom-Data-Set
    # Used TransferSyntax: UnknownTransferSyntax
    (0010,0010) PN [DOE^JOHN] # 12, 1 PatientName
    (0010,0020) LO [0290000] # 8, 1 PatientID

    2012-11-2917:09:55.209000 7728 INFO DIMSE receiveCommand
    2012-11-2917:09:55.209000 7728 INFO DIMSE receiveCommand: 1 pdv's (124 bytes), presID=3
    2012-11-2917:09:55.209000 7728 ERROR In DCXREQ, Code: 0, Text: DIMSE Command Failed
    Message Type : C-MOVE RSP
    Message ID Being Responded To : 1
    Affected SOP Class UID : MOVEStudyRootQueryRetrieveInformationModel
    Remaining Suboperations : none
    Completed Suboperations : none
    Failed Suboperations : none
    Warning Suboperations : none
    Data Set : none
    DIMSE Status : 0x0120: Unknown Status Code

    From here it says there's an unknown failure. I followed everything to the T. Any idea what this means?

    ReplyDelete
  2. Dre, You're sharp!
    * Add to the query object the QueryRetreiveLevel="PATIENT" and try again.
    0x0120 is sometime used for missing attribute

    If that doesn't help try using the MovePatient command with an accepter on a separate thread.

    MoveAndStore uses Study Root model and some PASC would not allow patient id only for that, you have to add study instance uid and make a study level query

    So in that case you have to use the move command
    If that doesn't help go through the following steps to verify that you are configured properly in the PACS
    1. Is your application configured in the PACS? Your AE Title is DREGO - is it configure with IP address and port number in the PACS?
    2. Set up a listener on your side (DCXACC class) Look at the example application Storage SCP example
    3. Go to the PACS and send C-ECHO to yourself

    ReplyDelete
  3. Thanks, I can't take too much credit though.. especially after all of those typo's I wrote! Your tutorial is excellent so I give you all the credit.

    I added the QueryRetreiveLevel and I no longer get an exception. But I checked and the image isn't there. I'm going to try following the suggestions you made with MovePatient. The first two sections of my log are identical but I've included the last section from my latest attempt.

    2012-11-2917:56:01.978000 6028 INFO
    C-MOVE Identifier:
    # Dicom-Data-Set
    # Used TransferSyntax: UnknownTransferSyntax
    (0008,0052) CS [PATIENT] # 8, 1 QueryRetrieveLevel
    (0010,0010) PN [DOE^JOHN] # 12, 1 PatientName
    (0010,0020) LO [0290000] # 8, 1 PatientID

    2012-11-2917:56:01.994000 6028 INFO DIMSE receiveCommand
    2012-11-2917:56:01.994000 6028 INFO DIMSE receiveCommand: 1 pdv's (118 bytes), presID=3

    Thanks!

    ReplyDelete
  4. Hi Roniza,

    Still unable to get images from PACS. I don't have access to the PACS server but the error code returned is:

    DIMSE Status : 0xc001: Error: Failed- Unable to process

    Whenever I try Move()/MoveAndStore().

    Others have mentioned a similiar problem is AFGA PACS. The MoveSeries does seem to return without error though. But I don't see how I can store Images after that. My Goal is simple, query PACS get series of images and download them. I feel like I'm close, any suggestions?

    ReplyDelete
    Replies
    1. Hi Dre
      In order to get the PACS to send you images back you have to configure it so it can resolve your AE Title to host name and port.
      The C-MOVE request only have the AE-TITLE
      The C-MOVE SCP must be able to take it and translate to ip address and port that it can connect back to and send the images.
      The only way to do this is to configure this information in the PACS.
      Every PACS have somewhere a list with AE Title, host and port. I assume that since you have no access to the PACS you couldn't configure it so that's maybe the case.
      Regards,
      Roni

      Delete
  5. Roniza,

    Thanks for the reply. I'm using the same AE Title that is registered on PACS from the same IP and since I'm able to Ping and Query with results back I think I'm okay there.

    The issue that was causing the exception was the fact I had the CallingAE and CalledAE reveresed in the MoveAndStore method! So now everything runs without exception but the images still aren't being moved to the moveandstore directory.

    I really like this DLL and hope we can use it.

    Here's a link to the code: http://txtup.co/7NqSU

    Anything glaringly wrong?

    Thanks,

    Dre

    ReplyDelete
  6. Hi, thanks for the post. They are very instructive.
    Something I can't understand is:
    I perform a C-MOVE request to get a set of images. For example all images given a patient name.
    I don't know what kind of images I'm going to receive. They could be of different modalities.
    The PACS will then answer sending me a C-STORE association request. C-STORE SOP classes are different for each modality.
    So there is, for instance, a CT Image Storage and a MR Image Storage class but there is no "Everything storage", that allows me to receive any kind of image. I can't understand the reason of this. Should my DICOM node declare to support every storage abstract syntax to obtain the same behaviour?

    Thanks!

    Daniele

    ReplyDelete
    Replies
    1. Hi Daniele
      You got it right.
      Every SOP Class UID should be negotiated separately.
      There's is something called Meta SOP Class UID that a group of related SOP Classes but nobody has defined a Meta SOP Class UID for 'Store any object'.
      It is used in DICOM Print to negotiate the 4 mandatory SOP Classes of the Print Service
      Roni

      Delete
  7. Hi, I am using the DCM4chee toolkit for a java implementation. I am trying to perform a query using a Private Tag element. I have had no such luck so far. Any help of how I could pull this off would be great. Thanks so much.

    ReplyDelete
  8. Hi Cody

    In short, the only way you are going to get results for your private tag is if you code the SCP on the other side. Its your private tag so you can't expect other implementations to support it.

    I'm not going to ask why are you query (DICOM C-FIND Command) using a private tag in the first place.

    DICOM is an open standard. Its a free country, you can query for whatever tag you like and the SCP is just as free to ignore it.

    As for dcm4chee, I'm not very familiar with it. I used it once or twice because I had to but most of the times I use RZDCX.

    If you want to code a DICOM server that support your private tags you can use DSRSVC with the DICOM Server API. See an example here: http://dicomiseasy.blogspot.co.il/2012/10/the-dicom-server-api.html

    Roni

    ReplyDelete
  9. Hi Roni:
    Thanks for your useful post.
    I write a software connected to GE HDx MRI using DICOM protocol. Is it possible to get the images generated by MRI in real-time?
    Thanks very much!

    Shengfa Zhang

    ReplyDelete
  10. Hi Roni,

    I am new to DICOM. I really need a help from you. I have registered the RZDCX lib and gave the reference. And when I build the application it went fine. But while executing it thrown an exception as:

    Additional information: Retrieving the COM class factory for component with CLSID {D0F02240-CB2C-468B-9DF5-E0FC4CA94839} failed due to the following error: 80040154 Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)).

    I am really stuck here. What should I do to overcome this?

    ReplyDelete
  11. Hi Roni,

    Thanks for this wonderful post, A a beginner to DICOM this post helped me a lot!

    But right now I am stuck up at something! I registered the RZDCX successfully and gave the reference in the project. But while running the application it is still throwing an exception :

    An unhandled exception of type 'System.Runtime.InteropServices.COMException' occurred in mscorlib.dll

    Additional information: Retrieving the COM class factory for component with CLSID {D0F02240-CB2C-468B-9DF5-E0FC4CA94839} failed due to the following error: 80040154 Class not registered (Exception from HRESULT: 0x80040154 (REGDB_E_CLASSNOTREG)).

    Please help me out here.

    Thanks in advance :)

    ReplyDelete
    Replies
    1. Hi James
      RZDCX is a COM object.
      You should use regsvr32 to register it in the registry.
      I suggest that you do this for both 32 and 64 versions.
      Roni

      Delete
    2. Hi Roni,

      Yes I have already registered it using regsvr32 and added the reference from COM object. My system is windows 7 - 64 bit! Any idea what am I missing?

      Delete
  12. Register on same computer both 32 and 64 versions

    ReplyDelete
  13. Hi,

    I have couple of doubts regarding the C-MOVE response.
    As per DICOM standard, once the C-MOVE request is sent to the PACS, which of the below step is correct( 1 or 2 ):-
    1. C-MOVE response followed by association request for C-STORE from PACS.
    2. Association request for C-STORE, perform C_STORE operation, finally send the C-MOVE response.

    Because we have implemented C-MOVE thinking that C-MOVE response will be sent first followed by C-STORE request. The implementation was working fine with PACS like DCM4CHEE etc. But when connected to original vendor's PACS, retrieve is failing.

    In which section of DICOM standard, can i see this in details?

    Thanks in advance.

    ReplyDelete
    Replies
    1. I prefer 2, though the DICOM standard doesn't specify the timing of pending C-MOVE Responses other then:
      "During the processing of the C-MOVE operation, the performing DIMSE-service-user may issue C-MOVE response primitives with a status of Pending."

      So standardwise, both are correct but I don't see a reason why to use 1 unless its a failed or refused status.

      BTW, though not recommended, you may just skip the whole pending responses all together as they are optional. Its not nice but some PACS do just that.

      I don't think that the problem you have with the 'vendors PACS' is due to the difference between 1 and 2 you describe and if it does, well, its a poor PACS implementation.

      Delete