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.
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.
Name | Action |
---|---|
C-Echo | Test the connection between two two devices |
C-Find | Query a remote device |
C-Move | Move an image between two remote devices |
C-Store | Send 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!