General Question about possibly using pynetdicom with Orthanc.

I have been exploring options for handling MPPS for MWL’s in Orthanc. It would be better to build a C++ plug-in or to extend the existing one, but I did start exploring the use of PynetDicom for this purpose. See: https://github.com/pydicom/pynetdicom and https://pydicom.github.io/pynetdicom/stable/examples/mpps.html, which might be nice as just an “add-on” independent python module.

That requires that the pip modules for pydicom and pynetdicom be installed on the Orthanc Docker Container, which I did, and some prelim testing indicates that there is at least some communication and handshaking going on (used some of the https://www.dvtk.org/ tools for that).

pynetdicom apparently runs its own little server, so I had to expose an additional port in my docker-compose.yml file (e.g. ports: [“4445:4242”, “xxxx:xxxx”], which works as far as MPPS listening is concerned. However, that breaks or overrides listening for DICOM messages on Orthanc (No connectivity via HOROS to Orthanc or from my RIS or in Orthanc Explorer). When I remove the Python code for the MPPS server, everything works again.

So just curious if anyone has tried that and if it is possible to use pynetdicom in a python script in conjunction with Orthanc as outlined above.

The example Python starter script that I tested is located here:

https://pydicom.github.io/pynetdicom/stable/examples/mpps.html#mpps-scp

There is more detailed information about the start_server method here: (e.g. blocking, etc.). I have not yet tried running the server in non-blocking mode in a separate thread, which might work actually.

https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.ae.ApplicationEntity.html#pynetdicom.ae.ApplicationEntity.start_server

Thanks.

That does seem to work now in non-blocking mode.

The Orthanc Log shows:

pacs-2_1 | D: Constructing Associate AC PDU
pacs-2_1 | # /usr/lib/python3.7/encodings/pycache/ascii.cpython-37.pyc matches /usr/lib/python3.7/encodings/ascii.py
pacs-2_1 | # code object from ‘/usr/lib/python3.7/encodings/pycache/ascii.cpython-37.pyc’
pacs-2_1 | import ‘encodings.ascii’ # <_frozen_importlib_external.SourceFileLoader object at 0x7f7400b899e8>
pacs-2_1 | In Pynet Dicom, N-CREATE
pacs-2_1 | Managed Instances
pacs-2_1 | {}

and I have attached a PDF of the DVTk log for sending an N-CREATE message. Might be worth exploring.

test.pdf (450 KB)

I’ve also attached the basic .py framework. You need to add those pip modules to the Orthanc Docker container when you build it. wkhtmltopdf if for creating pdf documents from html. It also had dcmtk, which is used by my python scripts sometimes. Not sure if that is already in or otherwise acccessible from the osimis/orthanc image, but I don’t think so.

The DockerFile for my container is like, which also has a few other PIP modules.

FROM osimis/orthanc
ENV DEBIAN_FRONTEND=noninteractive

disable http bundle since we’re specifying http parameters in the orthanc.json configuration file

ENV HTTP_BUNDLE_DEFAULTS=false

disable the auth defaults since we specify them in orthanc.json

ENV AC_BUNDLE_DEFAULTS=false

RUN pip3 install pydicom
RUN pip3 install pynetdicom
RUN pip3 install pdfkit
RUN pip3 install hl7
RUN pip3 install wkhtmltopdf
RUN pip3 install mysql-connector-python
RUN pip3 install requests

RUN mkdir /python

/python is bound to the host folder ./pacs-2/python, but Orthanc needs to be restarted to see changes.

/lua-scripts already exists in the container, and bound to lua in this folder. No need to restart to see changes, at least seems that way.

RUN apt-get update

RUN apt-get --assume-yes install wkhtmltopdf

These replace the above, QT support

asks for a keyboard type with xorg installation.

RUN apt-get --assume-yes install xvfb
RUN apt-get --assume-yes install openssl
RUN apt-get --assume-yes install xorg
RUN apt-get --assume-yes install libssl1.1
RUN apt-get --assume-yes install libcurl4

COPY wkhtmltox-0.12.4_linux-generic-amd64.tar.xz /
RUN tar xvJf wkhtmltox-0.12.4_linux-generic-amd64.tar.xz
RUN cp wkhtmltox/bin/wkhtmlto* /usr/bin/

RUN apt-get --assume-yes install dcmtk

COPY orthanc.json /etc/orthanc/
COPY osimis_viewer.json /etc/orthanc/
COPY stone.json /etc/orthanc/
COPY docker-entrypoint.sh /

mpps_framework.py (3.07 KB)

Hi Stephen,

Pynetdicom is an excellent tool. I don’t think it is best to run a Pynetdicom MPPS server as an orthanc plugin for the exact reason you discovered. Being a server, it needs to listen to incoming connections, and you will need to be careful how you do this given the Orthanc Python interpreter is single threaded.

You would be best to run the Pynetdicom MPPS process as a standalone container using a base container such as https://hub.docker.com/_/python. This way you will not block orthanc or run into conflicting port issues. You can communicate between the 2 containers by using Orthanc’s REST API.

Hope that helps.

James

Hey James, Thank you for the response. I actually did get it working if I set the server to non-blocking mode: ae.start_server((‘’, 11112), block=False, evt_handlers=handlers), such that Orthanc Explorer and at least some other things I tried still worked with the Orthanc Server. I think that makes it run in a different thread, although maybe not because it is part of the Orthanc Plug-in:

pydicom net docs

block (bool, optional) – If True (default) then the server will be blocking, otherwise it will start the server in a new thread and be non-blocking.

I actually put a message on the dicom group server here: [https://groups.google.com/g/comp.protocols.dicom/c/XzDCRlXHoVU](http://Dicom Forum Post) about some further testing that I did after changing it to non-blocking mode. If you are interested, maybe you could peek at that since it is specific to MWL’s & MPPS really and not so much Orthanc.

I tested it with DVTk, and the response that I get from the Python script as an Orthanc Python Plug-in script is shown in that thread.

It would actually be really nice if it would work as an Orthanc Python-Plugin rather than having to run a yet another container. I’m up to about 6 now (2 PACS, mysql, 2 postgres, nginx/php, phpmyadmin). I’ll test it out over the next few weeks since it seems like I have almost enough dev tools to work on it without needing to have a live modality. My question now is what should be returned in the response to the MPPS SCU N-Create request ? I get whatever the modality sends me, and I have the ability in the script to do all kinds of things really since I can get the full MWL file and edit any tags that I want accordingly, and then send a response back to the server. I need to edit a few tags with the current setup. Not that hard to actually add a MySQL DB for the MPPS stuff, although would still use the native Orthanc MWL plug-in. I am pretty new with all of the MPPS stuff, so not really sure how to respond to the N-Create and N-Set requests, as mentioned in my other thread.

Thanks.

Hello,

I have just added a sample in the Orthanc Book showing how to replace the built-in DICOM SCP of Orthanc by pynetdicom thanks to Python plugins:
https://book.orthanc-server.com/plugins/python.html#replacing-dicom-scp-of-orthanc-by-pynetdicom

Sébastien-

Posting response here per request:

Thanks for that. The setup that I had seemed to work so far, but sounds like you are saying it is better to basically “replace” the built-in Dicom server and then write my own handlers for the N-Create and N-Set events, or any other events that can be handled within the Server Block:

SCP = ae.start_server((‘’, port), block = False, evt_handlers = [
(pynetdicom.evt.EVT_C_STORE, HandleStore),
(pynetdicom.evt.EVT_N_CREATE, handle_create), # See my working script at the end of the post.

(pynetdicom.evt.EVT_N_SET, handle_set),
])

Is that “safer” than the other method of just running pydicomnet in a separate thread with ae.start_server((‘’, 11112), block=False, evt_handlers=handlers) set to a port different than the 4242 that is used for the built-in server and block=False ? That would have MPPS running on a separate dedicated port and possibly thread (I’m not sure how that works since the docs say that block=False starts the server in a separate thread, but that might not apply as a plug-in). I did try that with the Dicom Server enabled running on 4242 and initially that also seemed to work, so I would effectively have 2 dicom servers, one for Orthanc and the other to handle events that Orthanc does not support currently, like MPPS.

I did try your suggested setup, and after a bit of tweaking I can at least get a response with an N_CREATE event using DVTk, after adding the ae.add_supported_context(pynetdicom.sop_class.ModalityPerformedProcedureStepSOPClass). Not sure why I had to do that explicitly, otherwise it does not recognize that. I guess

If using the “dual” setup is not recommended, this arrangement seems to work.

These are snippets of that setup:

# PIP modules need to be installed in the Docker Container via the DockerFile build:

RUN pip3 install pydicom
RUN pip3 install pynetdicom

In the Python script:

import pynetdicom # https://github.com/pydicom/pynetdicom, sudo python3 -m pip install pynetdicom
import pydicom # https://github.com/pydicom/pydicom, sudo python3 -m pip install pydicom

# INIT and startup the server

ae = pynetdicom.AE()
ae.supported_contexts = pynetdicom.AllStoragePresentationContexts
ae.add_supported_context(pynetdicom.sop_class.ModalityPerformedProcedureStepSOPClass)
SCP = None

def OnChange(changeType, level, resource):

global SCP

if changeType == orthanc.ChangeType.ORTHANC_STARTED:

port = json.loads(orthanc.GetConfiguration()).get(‘DicomPort’, 4242)

SCP = ae.start_server((‘’, port), block = False, evt_handlers = [

(pynetdicom.evt.EVT_C_STORE, HandleStore),
(pynetdicom.evt.EVT_N_CREATE, handle_create),
(pynetdicom.evt.EVT_N_SET, handle_set),
])

orthanc.LogWarning(‘DICOM server using pynetdicom has started’)

elif changeType == orthanc.ChangeType.ORTHANC_STOPPED:

orthanc.LogWarning(‘Stopping pynetdicom’)
SCP.shutdown()

orthanc.RegisterOnChangeCallback(OnChange)

# MPPS SETUP

managed_instances = {}
pynetdicom.debug_logger()
# Implement the evt.EVT_N_CREATE handler
def handle_create(event):

MPPS’ N-CREATE request must have an Affected SOP Instance UID

print(“In Pynet Dicom, N-CREATE”)
req = event.request
print(req.AffectedSOPInstanceUID)
if req.AffectedSOPInstanceUID is None:

Failed - invalid attribute value

print(“Returning 0x0106, No AffectedSOPInstanceUID”)
return 0x0106, None

Can’t create a duplicate SOP Instance

if req.AffectedSOPInstanceUID in managed_instances:

Failed - duplicate SOP Instance

print(“returning 0x0111, duplicate SOP Instance”)
return 0x0111, None

The N-CREATE request’s Attribute List dataset

attr_list = event.attribute_list

Performed Procedure Step Status must be ‘IN PROGRESS’

if “PerformedProcedureStepStatus” not in attr_list:

Failed - missing attribute

print(“returning 0x0120, PerformedProcedureStepStatus not in Attribute List”)
return 0x0120, None
if attr_list.PerformedProcedureStepStatus.upper() != ‘IN PROGRESS’:
print(“returning 0x0106, Not IN PROGRESS”)
return 0x0106, None

Skip other tests…

Create a Modality Performed Procedure Step SOP Class Instance

DICOM Standard, Part 3, Annex B.17

print(“Getting DataSet”)
ds = pydicom.dataset.Dataset()
print(“Setting SOP UIDs”)

Add the SOP Common module elements (Annex C.12.1)

ds.SOPClassUID = pynetdicom.sop_class.ModalityPerformedProcedureStepSOPClass
ds.SOPInstanceUID = req.AffectedSOPInstanceUID
print(“Updating Attributes”)

Update with the requested attributes

ds.update(attr_list)

Add the dataset to the managed SOP Instances

managed_instances[ds.SOPInstanceUID] = ds
print(“Returned 0x0000 and DataSet”)
print (ds)

Return status, dataset

return 0x0000, ds

# Implement the evt.EVT_N_SET handler
def handle_set(event):
req = event.request
if req.RequestedSOPInstanceUID not in managed_instances:

Failure - SOP Instance not recognised

return 0x0112, None

ds = managed_instances[req.RequestedSOPInstanceUID]

The N-SET request’s Modification List dataset

mod_list = event.attribute_list

Skip other tests…

ds.update(mod_list)

Return status, dataset

return 0x0000, ds

Forgot to mention the issue that seems to be happening with loading of a python script that is not specified in the compose file:

“Finally, one thing I noticed is that I have this: ORTHANC__PYTHON_SCRIPT: “/python/combined.py” in my compose file, but I also left another renamed version of my combined.py file in the same folder (e.g. combined.bak.py) Apparently, orthanc was trying to load that also and it conflicted with my new version. Took me a bit to figure that out because I thought it would load only one script file. Does it automatically load all python scripts in that folder, the /python folder ? I have this mapping: ./pacs-2/python:/python in my docker compose as well and I have a few different .py files in that folder.”