DICOM Query/Retrieve (C-FIND, C-MOVE, C-GET)

Section 12 of 27
44% complete

Query/Retrieve operations allow you to search for and retrieve DICOM objects from remote systems.

C-FIND Query Service

C-FIND searches for studies, series, or images based on matching criteria.

Query Levels

Query/Retrieve Hierarchysaravanansubramanian.comfour query levels — each deeper query needs UIDs returned by the level above1PATIENT Levelquery patients by name, ID, date of birthreturns: Patient informationdrill down2STUDY Levelquery studies by date, modality, accession numberreturns: Study information3SERIES Levelquery series within a studyrequires Study Instance UID · returns: Series information4IMAGE Levelquery images within a seriesrequires Study + Series Instance UIDs · returns: Image/Instance information

Implementing Study-Level Query

Study-level queries are the most common starting point for searching a PACS archive. By specifying search criteria like patient name, date range, or modality, you can retrieve a list of matching studies. Each result includes the Study Instance UID needed for subsequent series and image queries.

public class DicomQueryService
{
    public List<DicomDataSet> QueryStudies(
        string callingAE,
        string calledAE,
        string host,
        int port,
        string patientName = "",
        string studyDate = "",
        string modality = "")
    {
        List<DicomDataSet> results = new List<DicomDataSet>();

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

        // Add Study Root Query/Retrieve presentation context
        association.AddPresentationContext(
            SOPClasses.StudyRootQueryRetrieve,
            DicomTransferSyntax.ExplicitVRLittleEndian
        );

        try
        {
            association.Open();

            // Create query dataset
            DicomDataSet query = CreateStudyLevelQuery(patientName, studyDate, modality);

            // Create C-FIND request
            DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_FIND_RQ);
            request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
            request.MessageID = 1;
            request.Priority = 0; // MEDIUM priority

            // Send query and collect results
            DicomCommandSet response = null;
            DicomDataSet responseData = null;

            do
            {
                response = association.SendRequest(request, query, out responseData);

                if (responseData != null && response.Status == 0xFF00) // Pending
                {
                    results.Add(responseData);
                    DisplayStudyResult(responseData);
                }

            } while (response.Status == 0xFF00); // Continue while pending

            if (response.Status == 0) // Success
            {
                Console.WriteLine($"\nQuery completed. Found {results.Count} studies.");
            }
            else
            {
                Console.WriteLine($"Query failed with status: 0x{response.Status:X4}");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Query error: {ex.Message}");
        }
        finally
        {
            if (association.IsOpen)
            {
                association.Close();
            }
        }

        return results;
    }

    private DicomDataSet CreateStudyLevelQuery(
        string patientName, string studyDate, string modality)
    {
        DicomDataSet query = new DicomDataSet();

        // Query/Retrieve Level - MUST specify
        query.Add(DicomTag.QueryRetrieveLevel, "STUDY");

        // Matching Keys (values to search for)
        query.Add(DicomTag.PatientName, patientName); // Empty = wildcard
        query.Add(DicomTag.StudyDate, studyDate);
        query.Add(DicomTag.ModalitiesInStudy, modality);

        // Return Keys (empty values = return these fields)
        query.Add(DicomTag.PatientID, "");
        query.Add(DicomTag.PatientBirthDate, "");
        query.Add(DicomTag.PatientSex, "");
        query.Add(DicomTag.StudyInstanceUID, "");
        query.Add(DicomTag.StudyTime, "");
        query.Add(DicomTag.AccessionNumber, "");
        query.Add(DicomTag.StudyDescription, "");
        query.Add(DicomTag.StudyID, "");
        query.Add(DicomTag.NumberOfStudyRelatedSeries, "");
        query.Add(DicomTag.NumberOfStudyRelatedInstances, "");

        return query;
    }

    private void DisplayStudyResult(DicomDataSet result)
    {
        Console.WriteLine("\n=== Study Found ===");
        Console.WriteLine($"Patient Name: {result[DicomTag.PatientName]?.Value}");
        Console.WriteLine($"Patient ID: {result[DicomTag.PatientID]?.Value}");
        Console.WriteLine($"Study Date: {result[DicomTag.StudyDate]?.Value}");
        Console.WriteLine($"Description: {result[DicomTag.StudyDescription]?.Value}");
        Console.WriteLine($"Modality: {result[DicomTag.ModalitiesInStudy]?.Value}");
        Console.WriteLine($"Study UID: {result[DicomTag.StudyInstanceUID]?.Value}");
    }
}

Series-Level Query

After identifying a study of interest, you can drill down to query its series. Series-level queries require the Study Instance UID and optionally filter by modality. This helps identify specific imaging sequences within a multi-series study.

public List<DicomDataSet> QuerySeries(
    string callingAE,
    string calledAE,
    string host,
    int port,
    string studyInstanceUID,
    string modality = "")
{
    List<DicomDataSet> results = new List<DicomDataSet>();

    // ... association setup ...

    // Create series level query
    DicomDataSet query = new DicomDataSet();
    query.Add(DicomTag.QueryRetrieveLevel, "SERIES");

    // Study Level - must include study UID
    query.Add(DicomTag.StudyInstanceUID, studyInstanceUID);

    // Series Level matching and return keys
    query.Add(DicomTag.SeriesInstanceUID, "");
    query.Add(DicomTag.Modality, modality);
    query.Add(DicomTag.SeriesNumber, "");
    query.Add(DicomTag.SeriesDescription, "");
    query.Add(DicomTag.NumberOfSeriesRelatedInstances, "");

    // ... send request and collect results ...

    return results;
}

Image-Level Query

Image-level queries return individual instance information within a series. This level requires both Study and Series Instance UIDs. Use image-level queries when you need to retrieve specific slices or verify which instances exist before retrieval.

public List<DicomDataSet> QueryImages(
    string callingAE,
    string calledAE,
    string host,
    int port,
    string studyInstanceUID,
    string seriesInstanceUID)
{
    List<DicomDataSet> results = new List<DicomDataSet>();

    // ... association setup ...

    // Create image level query
    DicomDataSet query = new DicomDataSet();
    query.Add(DicomTag.QueryRetrieveLevel, "IMAGE");

    // Required parent keys
    query.Add(DicomTag.StudyInstanceUID, studyInstanceUID);
    query.Add(DicomTag.SeriesInstanceUID, seriesInstanceUID);

    // Image Level return keys
    query.Add(DicomTag.SOPInstanceUID, "");
    query.Add(DicomTag.InstanceNumber, "");
    query.Add(DicomTag.SOPClassUID, "");

    // ... send request and collect results ...

    return results;
}

C-MOVE Retrieve Service

C-MOVE instructs the server to send images to a specified destination.

C-MOVE Architecture

C-MOVE Operation Flowsaravanansubramanian.coma three-party retrieve — the SCU never touches the images directlySCUquery requesterSCP · PACSholds the imagesDestination AEe.g. workstationC-MOVE requestMove Destination = destination AE title1SCP looks up matching studiesC-STORE sub-operationsone per instance, straight to the destination2Pending / progress responsesremaining · completed · failed counters3Final statussuccess (0x0000) or failure status code4

Implementing C-MOVE

The following implementation sends a C-MOVE request to retrieve a complete study. The MoveDestination parameter specifies where images should be sent. Ensure the destination AE Title is configured on the PACS server before initiating the move.

public class DicomRetrieveService
{
    public bool MoveStudy(
        string callingAE,
        string calledAE,
        string host,
        int port,
        string moveDestinationAE,  // Where to send the images
        string studyInstanceUID)
    {
        DicomAssociation association = new DicomAssociation
        {
            CallingAETitle = callingAE,
            CalledAETitle = calledAE,
            Host = host,
            Port = port
        };

        // Add Study Root Move presentation context
        association.AddPresentationContext(
            SOPClasses.StudyRootQueryRetrieve,
            DicomTransferSyntax.ExplicitVRLittleEndian
        );

        try
        {
            association.Open();

            // Create move dataset
            DicomDataSet moveData = new DicomDataSet();
            moveData.Add(DicomTag.QueryRetrieveLevel, "STUDY");
            moveData.Add(DicomTag.StudyInstanceUID, studyInstanceUID);

            // Create C-MOVE request
            DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_MOVE_RQ);
            request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
            request.MessageID = 1;
            request.Priority = 0;
            request.MoveDestination = moveDestinationAE;  // Critical: where to send

            // Send move request
            DicomCommandSet response;
            DicomDataSet responseData;

            do
            {
                response = association.SendRequest(request, moveData, out responseData);

                if (response.Status == 0xFF00) // Pending
                {
                    // Display progress
                    ushort remaining = response.NumberOfRemainingSuboperations;
                    ushort completed = response.NumberOfCompletedSuboperations;
                    ushort failed = response.NumberOfFailedSuboperations;

                    Console.WriteLine($"Progress: {completed} completed, " +
                        $"{remaining} remaining, {failed} failed");
                }

            } while (response.Status == 0xFF00);

            if (response.Status == 0)
            {
                Console.WriteLine("C-MOVE completed successfully");
                return true;
            }
            else
            {
                Console.WriteLine($"C-MOVE failed: Status 0x{response.Status:X4}");
                return false;
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"C-MOVE error: {ex.Message}");
            return false;
        }
        finally
        {
            if (association.IsOpen)
            {
                association.Close();
            }
        }
    }
}

C-GET Retrieve Service

C-GET retrieves images directly back to the requester (simpler than C-MOVE).

C-GET Architecture

C-GET Operation Flowsaravanansubramanian.comtwo-party retrieve — the SCP pushes C-STOREs back over the same associationSCUalso acts as C-STORE receiverSCP · PACSholds the imagesC-GET requeststudy / series / instance UIDs to retrieve1C-STORE sub-op · instance 12C-STORE sub-op · instance 23… one C-STORE per instance …Final C-GET responsecompleted / remaining / failed counts4Note — the SCU must negotiate a Storage SCP presentation context so it can receive the C-STOREs on the same association

Implementing C-GET

C-GET requires handling incoming C-STORE sub-operations from the SCP. You must register storage presentation contexts for all image types you expect to receive. The handler must respond to each incoming image to acknowledge receipt.

public class DicomGetService
{
    private List<DicomDataSet> retrievedInstances = new List<DicomDataSet>();

    public List<DicomDataSet> GetStudy(
        string callingAE,
        string calledAE,
        string host,
        int port,
        string studyInstanceUID)
    {
        retrievedInstances.Clear();

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

        // Add C-GET presentation context
        association.AddPresentationContext(
            SOPClasses.StudyRootQueryRetrieve,
            DicomTransferSyntax.ExplicitVRLittleEndian
        );

        // IMPORTANT: Also need storage presentation contexts to receive images
        association.AddPresentationContext(
            SOPClasses.CTImageStorage,
            DicomTransferSyntax.ExplicitVRLittleEndian
        );
        association.AddPresentationContext(
            SOPClasses.MRImageStorage,
            DicomTransferSyntax.ExplicitVRLittleEndian
        );

        // Handle incoming C-STORE requests from the SCP
        association.StoreRequest += OnStoreRequest;

        try
        {
            association.Open();

            // Create C-GET dataset
            DicomDataSet getData = new DicomDataSet();
            getData.Add(DicomTag.QueryRetrieveLevel, "STUDY");
            getData.Add(DicomTag.StudyInstanceUID, studyInstanceUID);

            // Create C-GET request
            DicomCommandSet request = new DicomCommandSet(DicomCommandType.C_GET_RQ);
            request.AffectedSOPClassUID = SOPClasses.StudyRootQueryRetrieve;
            request.MessageID = 1;
            request.Priority = 0;

            // Send C-GET request
            DicomCommandSet response;
            DicomDataSet responseData;

            do
            {
                response = association.SendRequest(request, getData, out responseData);

                if (response.Status == 0xFF00) // Pending
                {
                    ushort remaining = response.NumberOfRemainingSuboperations;
                    ushort completed = response.NumberOfCompletedSuboperations;

                    Console.WriteLine($"C-GET Progress: {completed}/{completed + remaining}");
                }

            } while (response.Status == 0xFF00);

            if (response.Status == 0)
            {
                Console.WriteLine($"Retrieved {retrievedInstances.Count} instances");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"C-GET error: {ex.Message}");
        }
        finally
        {
            if (association.IsOpen)
            {
                association.Close();
            }
        }

        return retrievedInstances;
    }

    private void OnStoreRequest(object sender, StoreRequestEventArgs e)
    {
        // Called when the server sends us an image via C-STORE
        try
        {
            retrievedInstances.Add(e.DataSet);
            Console.WriteLine($"Received: {e.DataSet[DicomTag.SOPInstanceUID]?.Value}");

            // Send success response
            DicomCommandSet response = new DicomCommandSet(DicomCommandType.C_STORE_RSP);
            response.Status = 0; // Success
            e.Association.SendResponse(response, null);
        }
        catch (Exception ex)
        {
            Console.WriteLine($"Error handling C-STORE: {ex.Message}");

            DicomCommandSet response = new DicomCommandSet(DicomCommandType.C_STORE_RSP);
            response.Status = 0xA700; // Out of resources
            e.Association.SendResponse(response, null);
        }
    }
}

Comparison: C-MOVE vs C-GET

AspectC-MOVEC-GET
DestinationThird partyBack to requester
SCU SCP requiredDestination must run SCPRequester handles storage
Legacy supportWidely supportedLess common
Firewall friendlyNeeds open ports on destMore firewall friendly
ArchitectureMore complexSimpler
Use caseMulti-destination routingDirect retrieval

Choose based on your architecture requirements and infrastructure constraints.

Learn more about DICOM query and retrieve operations:

Java Implementation:

.NET Implementation:

Testing Tools:

Quiz: DICOM Query/Retrieve (C-FIND, C-MOVE, C-GET)

Question 1 of 4

What does the QueryRetrieveLevel attribute specify in C-FIND?