DICOM Storage (C-STORE)

Section 13 of 27
48% complete

C-STORE is the fundamental DICOM service for sending images between systems.

Storage SCU Implementation

A Storage SCU sends DICOM objects to a Storage SCP. Building a robust storage client requires proper presentation context negotiation, error handling, and status code interpretation. The SCU must match the SOP Class of each image with an accepted presentation context.

Basic Storage Implementation

This implementation demonstrates sending a single DICOM file to a remote storage SCP. The presentation context is configured based on the image’s SOP Class to ensure compatibility.

public class DicomStorageService
{
    public bool StoreImage(
        string callingAE,
        string calledAE,
        string host,
        int port,
        string dicomFilePath)
    {
        // Read the DICOM file
        DicomImage image = new DicomImage(dicomFilePath);

        // Get SOP Class from the image
        string sopClassUID = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
        string sopInstanceUID = image.Attributes[DicomTag.SOPInstanceUID]?.Value.ToString();

        // Create association
        DicomAssociation association = new DicomAssociation
        {
            CallingAETitle = callingAE,
            CalledAETitle = calledAE,
            Host = host,
            Port = port
        };

        // Add presentation context for this SOP Class
        association.AddPresentationContext(
            sopClassUID,
            DicomTransferSyntax.ExplicitVRLittleEndian,
            DicomTransferSyntax.ImplicitVRLittleEndian
        );

        try
        {
            association.Open();

            // Check if presentation context was accepted
            if (!association.IsPresentationContextAccepted(sopClassUID))
            {
                Console.WriteLine($"SOP Class {sopClassUID} not supported by SCP");
                return false;
            }

            // Create C-STORE request
            DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_STORE_RQ);
            request.AffectedSOPClassUID = sopClassUID;
            request.AffectedSOPInstanceUID = sopInstanceUID;
            request.MessageID = 1;
            request.Priority = 0; // MEDIUM

            // Send the image
            DicomCommandSet response = association.SendRequest(request, image.DataSet);

            if (response.Status == 0)
            {
                Console.WriteLine($"Successfully stored: {sopInstanceUID}");
                return true;
            }
            else
            {
                Console.WriteLine($"Storage failed: Status 0x{response.Status:X4}");
                return false;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Storage error: {ex.Message}");
            return false;
        }
        finally
        {
            if (association.IsOpen)
            {
                association.Close();
            }
        }
    }
}

Batch Storage Implementation

When storing multiple images, reuse a single association for better performance rather than opening a new connection for each file. Group files by SOP Class to negotiate all required presentation contexts upfront.

public class BatchStorageService
{
    public StorageResults StoreBatch(
        string callingAE,
        string calledAE,
        string host,
        int port,
        List<string> dicomFiles)
    {
        StorageResults results = new StorageResults();

        // Group files by SOP Class for efficient association usage
        var filesBySopClass = GroupBySopClass(dicomFiles);

        DicomAssociation association = new DicomAssociation
        {
            CallingAETitle = callingAE,
            CalledAETitle = calledAE,
            Host = host,
            Port = port
        };

        // Add presentation contexts for all SOP Classes
        foreach (string sopClass in filesBySopClass.Keys)
        {
            association.AddPresentationContext(
                sopClass,
                DicomTransferSyntax.ExplicitVRLittleEndian,
                DicomTransferSyntax.ImplicitVRLittleEndian,
                DicomTransferSyntax.JPEGLossless
            );
        }

        try
        {
            association.Open();

            ushort messageID = 1;

            foreach (string filePath in dicomFiles)
            {
                try
                {
                    DicomImage image = new DicomImage(filePath);
                    string sopClassUID = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
                    string sopInstanceUID = image.Attributes[DicomTag.SOPInstanceUID]?.Value.ToString();

                    DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_STORE_RQ);
                    request.AffectedSOPClassUID = sopClassUID;
                    request.AffectedSOPInstanceUID = sopInstanceUID;
                    request.MessageID = messageID++;

                    DicomCommandSet response = association.SendRequest(request, image.DataSet);

                    if (response.Status == 0)
                    {
                        results.SuccessfulFiles.Add(filePath);
                    }
                    else
                    {
                        results.FailedFiles.Add(filePath,
                            $"Status: 0x{response.Status:X4}");
                    }
                }
                catch (Exception ex)
                {
                    results.FailedFiles.Add(filePath, ex.Message);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Association error: {ex.Message}");
        }
        finally
        {
            if (association.IsOpen)
            {
                association.Close();
            }
        }

        Console.WriteLine($"Storage Results: {results.SuccessfulFiles.Count} success, " +
            $"{results.FailedFiles.Count} failed");

        return results;
    }

    private Dictionary<string, List<string>> GroupBySopClass(List<string> files)
    {
        var groups = new Dictionary<string, List<string>>();

        foreach (string file in files)
        {
            try
            {
                using (DicomImage image = new DicomImage(file))
                {
                    string sopClass = image.Attributes[DicomTag.SOPClassUID]?.Value.ToString();
                    if (!groups.ContainsKey(sopClass))
                    {
                        groups[sopClass] = new List<string>();
                    }
                    groups[sopClass].Add(file);
                }
            }
            catch { }
        }

        return groups;
    }
}

public class StorageResults
{
    public List<string> SuccessfulFiles { get; set; } = new List<string>();
    public Dictionary<string, string> FailedFiles { get; set; } = new Dictionary<string, string>();
}

Storage SCP Implementation

A Storage SCP receives and stores DICOM objects sent by modalities or other systems. The SCP must handle association negotiation, accept appropriate SOP Classes, receive image data, and persist files to storage. Proper error handling ensures graceful responses even when storage fails.

Basic Storage SCP

The following implementation creates a storage server that organizes received files by Patient ID, Study UID, and Series UID. This hierarchical structure facilitates efficient file management and retrieval.

public class DicomStorageServer
{
    private DicomServer server;
    private string storageDirectory;
    private string aeTitle;

    public DicomStorageServer(string aeTitle, int port, string storageDirectory)
    {
        this.aeTitle = aeTitle;
        this.storageDirectory = storageDirectory;

        Directory.CreateDirectory(storageDirectory);

        // Create server
        server = new DicomServer
        {
            AETitle = aeTitle,
            Port = port
        };

        // Handle association requests
        server.AssociationRequested += OnAssociationRequested;

        // Handle C-STORE requests
        server.StoreRequest += OnStoreRequest;
    }

    private void OnAssociationRequested(object sender, AssociationRequestEventArgs e)
    {
        Console.WriteLine($"Association request from {e.CallingAETitle}");

        // Accept all common storage SOP Classes
        foreach (var pc in e.PresentationContexts)
        {
            // Accept if we support the SOP Class
            if (IsSupportedSOPClass(pc.AbstractSyntax))
            {
                pc.Result = PresentationContextResult.Accept;

                // Select preferred transfer syntax
                if (pc.TransferSyntaxes.Contains(DicomTransferSyntax.ExplicitVRLittleEndian))
                {
                    pc.SelectedTransferSyntax = DicomTransferSyntax.ExplicitVRLittleEndian;
                }
                else
                {
                    pc.SelectedTransferSyntax = pc.TransferSyntaxes[0];
                }
            }
            else
            {
                pc.Result = PresentationContextResult.RejectAbstractSyntaxNotSupported;
            }
        }

        e.Accept();
    }

    private void OnStoreRequest(object sender, StoreRequestEventArgs e)
    {
        try
        {
            // Extract key information
            string patientID = e.DataSet[DicomTag.PatientID]?.Value?.ToString() ?? "UNKNOWN";
            string studyUID = e.DataSet[DicomTag.StudyInstanceUID]?.Value?.ToString() ?? "UNKNOWN";
            string seriesUID = e.DataSet[DicomTag.SeriesInstanceUID]?.Value?.ToString() ?? "UNKNOWN";
            string sopInstanceUID = e.DataSet[DicomTag.SOPInstanceUID]?.Value?.ToString() ?? "UNKNOWN";

            // Create directory structure: PatientID/StudyUID/SeriesUID/
            string patientDir = Path.Combine(storageDirectory, SanitizeFileName(patientID));
            string studyDir = Path.Combine(patientDir, SanitizeFileName(studyUID));
            string seriesDir = Path.Combine(studyDir, SanitizeFileName(seriesUID));

            Directory.CreateDirectory(seriesDir);

            // Save the DICOM file
            string filePath = Path.Combine(seriesDir, $"{sopInstanceUID}.dcm");
            e.DataSet.Save(filePath);

            Console.WriteLine($"Stored: {filePath}");

            // Send success response
            e.SendResponse(0); // Success status
        }
        catch (IOException ex)
        {
            Console.WriteLine($"Storage error: {ex.Message}");
            e.SendResponse(0xA700); // Out of resources
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Storage error: {ex.Message}");
            e.SendResponse(0xC000); // Cannot understand
        }
    }

    private bool IsSupportedSOPClass(string sopClassUID)
    {
        var supportedClasses = new[]
        {
            "1.2.840.10008.5.1.4.1.1.2",     // CT Image Storage
            "1.2.840.10008.5.1.4.1.1.4",     // MR Image Storage
            "1.2.840.10008.5.1.4.1.1.6.1",   // US Image Storage
            "1.2.840.10008.5.1.4.1.1.7",     // Secondary Capture
            "1.2.840.10008.5.1.4.1.1.1",     // CR Image Storage
            "1.2.840.10008.5.1.4.1.1.1.1",   // Digital X-Ray
            "1.2.840.10008.5.1.4.1.1.12.1",  // X-Ray Angio
            "1.2.840.10008.5.1.4.1.1.128"    // PET Image Storage
        };

        return supportedClasses.Contains(sopClassUID);
    }

    private string SanitizeFileName(string name)
    {
        foreach (char c in Path.GetInvalidFileNameChars())
        {
            name = name.Replace(c, '_');
        }
        return name;
    }

    public void Start()
    {
        server.Start();
        Console.WriteLine($"Storage SCP started: {aeTitle} on port {server.Port}");
    }

    public void Stop()
    {
        server.Stop();
        Console.WriteLine("Storage SCP stopped");
    }
}

Storage Status Codes

StatusNameDescription
0x0000SuccessStorage completed successfully
0xA700Out of ResourcesCannot store due to resource constraints
0xA900Data Set Does Not Match SOP ClassMismatch between data and declared SOP Class
0xC000Cannot UnderstandUnable to process the request
0xB000WarningCoercion of Data Elements
0xB007WarningData Set Does Not Match SOP Class (Elements Coerced)

Image Routing

Route images from sources to multiple destinations based on configurable rules. DICOM routers are essential components in enterprise imaging environments where images from modalities need to reach multiple systems like PACS, workstations, and cloud archives.

DICOM Router Implementation

This router receives images via its storage SCP, evaluates routing rules based on image metadata, and forwards matching images to configured destinations. Failed transmissions are queued for retry to ensure delivery.

public class DicomRouter
{
    private DicomStorageServer inboundServer;
    private List<RoutingRule> rules;
    private string tempStorageDirectory;

    public DicomRouter(string aeTitle, int port, string tempDirectory)
    {
        tempStorageDirectory = tempDirectory;
        rules = new List<RoutingRule>();

        inboundServer = new DicomStorageServer(aeTitle, port, tempDirectory);
        inboundServer.ImageReceived += OnImageReceived;
    }

    public void AddRoutingRule(RoutingRule rule)
    {
        rules.Add(rule);
    }

    private async void OnImageReceived(object sender, ImageReceivedEventArgs e)
    {
        Console.WriteLine($"Routing image: {e.SOPInstanceUID}");

        foreach (var rule in rules)
        {
            if (rule.Matches(e.DataSet))
            {
                await RouteToDestination(e.FilePath, rule.Destination);
            }
        }
    }

    private async Task RouteToDestination(string filePath, DicomDestination dest)
    {
        try
        {
            var storageService = new DicomStorageService();
            bool success = storageService.StoreImage(
                dest.CallingAE,
                dest.AETitle,
                dest.Host,
                dest.Port,
                filePath
            );

            if (success)
            {
                Console.WriteLine($"Routed to {dest.AETitle}");
            }
            else
            {
                // Queue for retry
                QueueForRetry(filePath, dest);
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Routing error: {ex.Message}");
            QueueForRetry(filePath, dest);
        }
    }

    private void QueueForRetry(string filePath, DicomDestination dest)
    {
        // Add to retry queue for later processing
        Console.WriteLine($"Queued for retry: {filePath} to {dest.AETitle}");
    }

    public void Start()
    {
        inboundServer.Start();
        Console.WriteLine("DICOM Router started");
    }

    public void Stop()
    {
        inboundServer.Stop();
    }
}

public class RoutingRule
{
    public string Name { get; set; }
    public string ModalityFilter { get; set; }
    public string BodyPartFilter { get; set; }
    public DicomDestination Destination { get; set; }

    public bool Matches(DicomDataSet dataSet)
    {
        // Check modality
        if (!string.IsNullOrEmpty(ModalityFilter))
        {
            string modality = dataSet[DicomTag.Modality]?.Value?.ToString() ?? "";
            if (modality != ModalityFilter)
                return false;
        }

        // Check body part
        if (!string.IsNullOrEmpty(BodyPartFilter))
        {
            string bodyPart = dataSet[DicomTag.BodyPartExamined]?.Value?.ToString() ?? "";
            if (!bodyPart.Contains(BodyPartFilter))
                return false;
        }

        return true;
    }
}

public class DicomDestination
{
    public string Name { get; set; }
    public string CallingAE { get; set; }
    public string AETitle { get; set; }
    public string Host { get; set; }
    public int Port { get; set; }
}

Router Usage Example

Configure the router with rules that match images based on modality, body part, or other DICOM attributes. Each rule specifies a destination where matching images should be forwarded.

// Create router
var router = new DicomRouter("ROUTER", 11112, @"C:\DICOM\Routing");

// Add routing rules
router.AddRoutingRule(new RoutingRule
{
    Name = "All CT to PACS",
    ModalityFilter = "CT",
    Destination = new DicomDestination
    {
        Name = "Main PACS",
        CallingAE = "ROUTER",
        AETitle = "PACS_SERVER",
        Host = "192.168.1.100",
        Port = 104
    }
});

router.AddRoutingRule(new RoutingRule
{
    Name = "Chest X-Ray to Workstation",
    ModalityFilter = "CR",
    BodyPartFilter = "CHEST",
    Destination = new DicomDestination
    {
        Name = "Reading Workstation",
        CallingAE = "ROUTER",
        AETitle = "WORKSTATION_1",
        Host = "192.168.1.50",
        Port = 11112
    }
});

// Start router
router.Start();

Summary

C-STORE is fundamental to DICOM workflows:

  1. SCU Implementation: Send images to remote systems
  2. SCP Implementation: Receive and store incoming images
  3. Batch Processing: Efficiently send multiple images
  4. Image Routing: Forward images to multiple destinations
  5. Error Handling: Proper status code handling and retry logic

These capabilities form the backbone of medical imaging data distribution.

Learn more about DICOM storage operations:

Java Implementation:

.NET Implementation:

Error Handling:

Quiz: DICOM Storage (C-STORE)

Question 1 of 4

What does C-STORE status 0x0000 indicate?