Categories
Team Members

DICOM Networking in Python

Sixth post in a series of posts written by members of the Qurit Team. Written by Adam Watkins.

Part 1: The Handshake

At Qurit, our research relies heavily on analyzing medical images from PET and SPECT scanners. These images, and the majority of all clinical images, come in the DICOM file format. A somewhat underrated yet needed part of the research process is the automation of image retrieval and anonymization. This is an important step before clinical images can be used to train and test our machine learning models. Using DICOM’s underlying networking protocol, imaging pipelines can be created to automate the process of image acquisition, anonymization, and analysis.

Under the hood, DICOM is not only a file format (.dcm) but also a fully featured, and admittedly somewhat archaic, networking protocol. The DICOM networking protocol resides just above the TCP layer in the OSI model. Built upon TCP/IP, the DICOM protocol relies upon making a handshake before communication between two remotes can begin. In DICOM this is called creating an association.

The DICOM Protocol Network Stack

In this post I will show how to associate with a remote DICOM server such as PACS (Picture Archiving and Communication System) server or PET scanner. I will also show how to send a C-Echo to the remote associated with; the first of four basic actions. After the C-Echo, I will move on to the other, increasingly advanced, actions shown below.

NameAction
C-EchoTest the connection between two two devices
C-FindQuery a remote device
C-MoveMove an image between two remote devices
C-StoreSend and Image from a local device to a remote device for storage

Preforming a C-Echo

This article and all future articles will be using pynetdicom (1.5.1). Future articles will also be using pydicom (1.4.1). Pynetdicom handles the low level networking implementation whilst pydiom will allow us to read, write, query DICOM files.

from dataclasses import dataclass

from pynetdicom.sop_class import VerificationSOPClass
from pynetdicom import AE

First we are going to create some helper classes.

@dataclass
class Modality:
    addr: str
    port: int
    ae_title: str

class Association:
    def __init__(self, modality, context):
        self.modality = modality
        self.context = context

    def __enter__(self):
        ae = AE()
        ae.add_requested_context(self.context)
        self._association = ae.associate(**vars(self.modality))
        return self._association

    def __exit__(self, *args):
        self._association.release()
        self._association = None

Above, we are first creating a modality class to keep all our modalities organized. For our purposes, a modality is a remote that we will be connecting with. Each remote has an address, a port, and an application entity (AE) title.

Second, and more importantly, we are creating an association class to act as a context manger; helping to create and release associations. This is accomplished by implementing both dunder enter and dunder exit. Even though more complex initially, I believe using a context manger is good practice as it ensures that releasing associations will never be forgotten (even under error conditions). As an added bonus, it will make all future code shorter, cleaner and frankly more pythonic.

if __name__ == '__main__':
    modality = Modality('127.0.0.1', 104, 'DISCOVERY')

    with Association(modality, VerificationSOPClass) as assoc:
        resp = assoc.send_c_echo()
        print(f'Modality responded with status: {resp.Status}')

Here we are finally testing our connection with the modality that we are trying to reach. If all goes well, the modality should respond with the successful status code 0x0000. Notice how by using context management, we are able to release the association automatically.

If we were to forgo using automatic context management, the implementation would look like the code below. It would be imperative to ensure that association is always released to prevent tying up valuable server resources.

if __name__ == '__main__':
    ae = AE()
    ae.add_requested_context(VerificationSOPClass)
    assoc = ae.associate('127.0.0.1', 104, ae_title='DISCOVERY')
    
    try:
        resp = assoc.send_c_echo()
        print(f'Modality responded with status: {resp.Status}')
        assoc.release()
    except Exception as e:
        assoc.release()
        raise e

Stay tuned for part two where I demonstrate how query a remote device with a C-Find. In the meantime, check out Qurit on Github!

One reply on “DICOM Networking in Python”

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s