DICOM Basics using .NET and C# - Push Operations (C-STORE)


How the C-STORE 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. My DICOM networking-related tutorials on DICOM Verification and on DICOM Associations should also provide some foundations to help 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 look at a DICOM composite service called C-STORE that helps push data to a remote server for storage (see my tutorial on DICOM Associations if you want to understand what 'composite' and 'normalized' services are). We are going to explore how to build a C-STORE SCU client that can perform such an operation.

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.

“Make your interests gradually wider and more impersonal, until bit by bit the walls of the ego recede, and your life becomes increasingly merged in the universal life. An individual human existence should be like a river — small at first, narrowly contained within its banks, and rushing passionately past rocks and over waterfalls. Gradually the river grows wider, the banks recede, the waters flow more quietly, and in the end, without any visible break, they become merged in the sea, and painlessly lose their individual being.” ~ Bertrand Russell

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
  • Also included with the source code is the Orthanc Server configuration file (orthanc.json) for your reference

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 can download one of the many open source PACS servers available on the Internet. Orthanc Server is one such tool and is relatively straightforward set up and get going. Please see my article on getting started with Orthanc Server for more information.

Pushing Data for Storage using C-STORE

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 one another 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 instance, when a CT scanner decides to transmit a series of images to the PACS storage server, it establishes an association first, and then send a 'C-STORE-RQ' DIMSE command followed by a CT image IOD instance that needs to be stored. The C-Store SCP responds back with a success or a failure response (using a 'C-STORE-RSP' object) indicating the result of the operation. The C-Store SCU may continue to transmit a series of more commands for every image that it requires to be store, and the process continues back and forth between the two devices along the same pattern.

DICOM Store Service Diagram

The structure of the C-Store 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 client may also initiate a cancel operation anytime while the store operation is in progress (using a 'C-CANCEL-RQ' command) at which time the C-STORE-SCP will then cancel its store operation and return 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 Store Request and Response

“To delight a child, to add a new joy to the crowded miracles of childhood, is no less worth doing than to leave a Sistine Chapel to astound a somewhat bored procession of tourists; or to have written a classic that sells by the thousands and is possessed unread by all save an infinitesimal percentage of its owners. It is, then, not an ignoble thing to do one’s very best to give our coming rulers – children – a taste of the Kingdom of Art..” ~ Gleeson White

Example of C-STORE Operation

Let us have quick look at some code now. Here, I create a factory method that provides a DicomClient object with a C-Store request attached to it. I then attach some event handlers to the DICOM client to let us be notified of various events that occur during association negotiation, store operation responses and also association termination. I then initiate the store operation on the DicomClient object by invoking its Send method passing in the path of the SOP instance that we want to transmit for storage to Orthanc Server. There are many convenient methods and attributes returned in the event handlers to give more insight into many aspects of the DICOM negotiation and store operation such as the abstract syntax, the transfer syntax and meta data for the SOP instance being transmitted for the store operation. I will let you explore those on your own.

    using Dicom.Network;
    using System;
    using System.Diagnostics;
    using System.IO;

    namespace DICOMEchoVerificationWithOrthancServer
    {
        public class Program
        {
            private static readonly string PathToDicomTestFile = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Test Files", "0002.dcm");

            static void Main(string[] args)
            {
                try
                {
                    var dicomRemoteHost = "localhost";
                    var dicomRemoteHostPort = 4242;
                    var useTls = false;
                    var ourDotNetTestClientDicomAeTitle = "OurDotNetTestClient";
                    var remoteDicomHostAeTitle = "ORTHANC";

                    //create DICOM store SCU client with handlers
                    var client = CreateDicomStoreClient(PathToDicomTestFile);

                    //send the verification request to the remote DICOM server
                    client.Send(dicomRemoteHost, dicomRemoteHostPort, useTls, ourDotNetTestClientDicomAeTitle, remoteDicomHostAeTitle);
                    LogToDebugConsole("Our DICOM CStore operation was successfully completed");
                }
                catch (Exception e)
                {
                    LogToDebugConsole($"Error occured during DICOM verification request -> {e.StackTrace}");
                }
            }

            private static DicomClient CreateDicomStoreClient(string fileToTransmit)
            {
                var client = new DicomClient();

                //request for DICOM store operation
                var dicomCStoreRequest = new DicomCStoreRequest(fileToTransmit);

                //attach an event handler when remote peer responds to store request 
                dicomCStoreRequest.OnResponseReceived += OnStoreResponseReceivedFromRemoteHost;
                client.AddRequest(dicomCStoreRequest);

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

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

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

                return client;
            }

            private static void OnStoreResponseReceivedFromRemoteHost(DicomCStoreRequest request, DicomCStoreResponse response)
            {
                LogToDebugConsole("DICOM Store request was received by remote host for storage...");
                LogToDebugConsole($"DICOM Store request was received by remote host for SOP instance transmitted for storage:{request.SOPInstanceUID}");
                LogToDebugConsole($"Store operation response status returned was:{response.Status}");
            }

            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)
            {
                Debug.WriteLine(informationToLog);
            }
        }
    }

“Dreaming, after all, is a form of planning.” ~ Gloria Steinem

Results of running the code example is shown below. As you can see, the association is first established and then the C-STORE-SCP (the Orthanc Server in our case) responds back with the status of the store operation. The association is then released and our entire operation is successfully completed. Please note that one cannot 'batch' files for transmission, and each file has to be transmitted one by one. However, it is common to see some convenient abstractions on top of the C-STORE operation to enable bulk file transmissions in DICOM software. Please note that if the file you are transmitting already exists on the C-STORE SCP, you may receive either a warning or an error, and this is perfectly okay and you should design your software to continue to transmit the rest of the images and not halt the operation entirely as a result of a single error. Please see DICOM documentation on more guidance and tips on this aspect.


Association was accepted by:localhost
DICOM Store request was received by remote host for storage...
DICOM Store request was received by remote host for SOP instance transmitted for storage:Unknown [1.3.12.2.1107.5.4.3.321890.19960124.162922.29]
Store operation response status returned was:Success
Association was released. BYE BYE!
Our DICOM CStore operation was successfully completed

Testing Tools for DICOM Store Operations

When you need to troubleshoot store 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 storescu command is the one that I will use here to push a local DICOM file to Dr. Dave Harvey's free online DICOM test server provided here. The remote server is listening to requests on port 104 in this example. The console output generated when running this command is shown below for reference.


C:\SaravananDicomTestingTools\DCMTK-3.6.5-Win64-Dynamic\bin>storescu.exe -aet OurStoreScu "www.dicomserver.co.uk" 104 -aec MEDCONNECTIONS C:\DICOMImages\1.2.826.0.1.3680043.11.106\2.25.101703809854595919801950834747690813074.dcm -v
I: checking input files ...
I: Requesting Association
I: Association Accepted (Max Send PDV: 65524)
I: Sending file: C:\Junk\DICOMImages\1.2.826.0.1.3680043.11.106\2.25.101703809854595919801950834747690813074.dcm
I: Converting transfer syntax: Little Endian Explicit -> Little Endian Explicit
I: Sending Store Request (MsgID 1, OPb)
XMIT: .........................................................................................................................................................................
I: Received Store Response (Unknown Status: 0x111)
I: Releasing Association

Conclusion

This concludes the article on how the C-STORE operation works in DICOM. This is one of the most important operations in DICOM, and is often used to transmit DICOM images from MRIs, CTs and other modalities to PACS serves and other remote destinations. In my next tutorial in this series on the DICOM standard, I will cover how other composite operations including "C-GET" and "C-MOVE" employ the "C-STORE" as a 'sub-operation' behind the scenes during image retrieval operations as well. See you then!