Query/Retrieve (Move) Service Examples

The DICOM Query/Retrieve Service provides a mechanism for a service user to query and retrieve the SOP Instances managed by a QR SCP. The QR (Move) SOP classes allow an SCU to request an SCP send matching SOP Instances to a known Storage SCP over a new association. This is accomplished through the DIMSE C-MOVE and C-STORE services.

One limitation of the C-MOVE service is that the Move SCP/Storage SCU must know in advance the details (AE title, IP address, port number) of the destination Storage SCP. If the Move SCP doesn’t know the destination AE then it will usually respond with an 0xA801 status code.

Query/Retrieve (Move) SCU

Associate with a peer DICOM Application Entity and request it send all SOP Instances for the patient with Patient ID 1234567 belonging to the series with Study Instance UID 1.2.3 and Series Instance UID 1.2.3.4 to a Storage SCP with AE title 'STORE_SCP'.

from pydicom.dataset import Dataset

from pynetdicom import AE, debug_logger
from pynetdicom.sop_class import PatientRootQueryRetrieveInformationModelMove

debug_logger()

# Initialise the Application Entity
ae = AE()

# Add a requested presentation context
ae.add_requested_context(PatientRootQueryRetrieveInformationModelMove)

# Create out identifier (query) dataset
ds = Dataset()
ds.QueryRetrieveLevel = 'SERIES'
# Unique key for PATIENT level
ds.PatientID = '1234567'
# Unique key for STUDY level
ds.StudyInstanceUID = '1.2.3'
# Unique key for SERIES level
ds.SeriesInstanceUID = '1.2.3.4'

# Associate with peer AE at IP 127.0.0.1 and port 11112
assoc = ae.associate("127.0.0.1", 11112)

if assoc.is_established:
    # Use the C-MOVE service to send the identifier
    responses = assoc.send_c_move(ds, 'STORE_SCP', PatientRootQueryRetrieveInformationModelMove)
    for (status, identifier) in responses:
        if status:
            print('C-MOVE query status: 0x{0:04x}'.format(status.Status))
        else:
            print('Connection timed out, was aborted or received invalid response')

    # Release the association
    assoc.release()
else:
    print('Association rejected, aborted or never connected')

The responses received from the SCP include notifications on whether or not the storage sub-operations have been successful.

In the next example we use a Storage SCP running within the same AE as the Move Destination. Remember that the Move SCP must first be configured with the IP and port number of the corresponding AE title. Check the handler implementation documentation to see the requirements for the evt.EVT_C_STORE handler.

from pydicom.dataset import Dataset

from pynetdicom import AE, evt, StoragePresentationContexts, debug_logger
from pynetdicom.sop_class import PatientRootQueryRetrieveInformationModelMove

debug_logger()

def handle_store(event):
    """Handle a C-STORE service request"""
    # Ignore the request and return Success
    return 0x0000

handlers = [(evt.EVT_C_STORE, handle_store)]

# Initialise the Application Entity
ae = AE()

# Add a requested presentation context
ae.add_requested_context(PatientRootQueryRetrieveInformationModelMove)

# Add the Storage SCP's supported presentation contexts
ae.supported_contexts = StoragePresentationContexts

# Start our Storage SCP in non-blocking mode, listening on port 11120
ae.ae_title = 'OUR_STORE_SCP'
scp = ae.start_server(("127.0.0.1", 11120), block=False, evt_handlers=handlers)

# Create out identifier (query) dataset
ds = Dataset()
ds.QueryRetrieveLevel = 'SERIES'
# Unique key for PATIENT level
ds.PatientID = '1234567'
# Unique key for STUDY level
ds.StudyInstanceUID = '1.2.3'
# Unique key for SERIES level
ds.SeriesInstanceUID = '1.2.3.4'

# Associate with peer AE at IP 127.0.0.1 and port 11112
assoc = ae.associate("127.0.0.1", 11112)

if assoc.is_established:
    # Use the C-MOVE service to send the identifier
    responses = assoc.send_c_move(ds, 'OUR_STORE_SCP', PatientRootQueryRetrieveInformationModelMove)

    for (status, identifier) in responses:
        if status:
            print('C-MOVE query status: 0x{0:04x}'.format(status.Status))
        else:
            print('Connection timed out, was aborted or received invalid response')

    # Release the association
    assoc.release()
else:
    print('Association rejected, aborted or never connected')

# Stop our Storage SCP
scp.shutdown()

Query/Retrieve (Move) SCP

The following represents a toy implementation of a Query/Retrieve (Move) SCP where the SCU has sent the following Identifier dataset under the Patient Root Query Retrieve Information Model - Move context and the move destination AE title "STORE_SCP" is known to correspond to the IP address ``127.0.0.1 and listen port number 11113.

ds = Dataset()
ds.QueryRetrieveLevel = 'PATIENT'
ds.PatientID = '1234567'

This is a very bad way of managing stored SOP Instances, in reality its probably best to store the instance attributes in a database and run the query against that, which is the approach taken by the qrscp application.

Check the handler implementation documentation to see the requirements for the evt.EVT_C_MOVE handler.

import os

from pydicom import dcmread
from pydicom.dataset import Dataset

from pynetdicom import AE, StoragePresentationContexts, evt
from pynetdicom.sop_class import PatientRootQueryRetrieveInformationModelMove

# Implement the evt.EVT_C_MOVE handler
def handle_move(event):
    """Handle a C-MOVE request event."""
    ds = event.identifier

    if 'QueryRetrieveLevel' not in ds:
        # Failure
        yield 0xC000, None
        return

    # get_known_aet() is here to represent a user-implemented method of
    #   getting known AEs, for this example it returns a dict with the
    #   AE titles as keys
    known_aet_dict = get_known_aet()
    try:
        (addr, port) = known_aet_dict[event.move_destination]
    except KeyError:
        # Unknown destination AE
        yield (None, None)
        return

    # Yield the IP address and listen port of the destination AE
    yield (addr, port)

    # Import stored SOP Instances
    instances = []
    matching = []
    fdir = '/path/to/directory'
    for fpath in os.listdir(fdir):
        instances.append(dcmread(os.path.join(fdir, fpath)))

    if ds.QueryRetrieveLevel == 'PATIENT':
        if 'PatientID' in ds:
            matching = [
                inst for inst in instances if inst.PatientID == ds.PatientID
            ]

        # Skip the other possible attributes...

    # Skip the other QR levels...

    # Yield the total number of C-STORE sub-operations required
    yield len(matching)

    # Yield the matching instances
    for instance in matching:
        # Check if C-CANCEL has been received
        if event.is_cancelled:
            yield (0xFE00, None)
            return

        # Pending
        yield (0xFF00, instance)

handlers = [(evt.EVT_C_MOVE, handle_move)]

# Create application entity
ae = AE()

# Add the requested presentation contexts (Storage SCU)
ae.requested_contexts = StoragePresentationContexts
# Add a supported presentation context (QR Move SCP)
ae.add_supported_context(PatientRootQueryRetrieveInformationModelMove)

# Start listening for incoming association requests
ae.start_server(("127.0.0.1", 11112), evt_handlers=handlers)

It’s also possible to get more control over the association with the Storage SCP that’ll be receiving any matching datasets by yielding (addr, port, kwargs) instead of (addr, port), where kwargs is a dict containing keyword parameters that’ll be passed to AE.associate(). In particular, this allows you to tailor the presentation contexts that will be requested to the datasets matching the query (via the contexts keyword parameter).