Load DICOM data via "networking" with python script?

Hi,

what would be the recommended way to realize DICOM network access via python script? We would need to load all CT scans of a patient (defined by patient ID) of a certain time period into Slicers DICOM database, then process data later. DICOM networking via GUI works well here.

Thanks and best regards
Rudolf

Hi @rbumm -

I’m guessing you mean traditional dicom networking (a.k.a. “DIMSE”) since that is the very common standard, compared to the newer DICOMweb standard (if your PACS or VNA supports DICOMweb that’s cleaner).

For DIMSE, Slicer uses a combination of C++ and python. The CFIND, CMOVE type transactions are in CTK C++ but should all be exposed and usable in python. For the listener (CSTORE SCP) we use the dcmtk command line executable in a separate process in python.

If your PACS supports CGET, you are probably best to use getscu into a temp directory and then load into the database. Let us know if you need more pointers.

Thank you Steve.

To be able to pull from our GE-PACS, we needed to

  • specify Calling AETitle
  • specify the StorageAETitle
  • specify three port adresses
  • the 4100 port
  • disable CGET

in slicers DICOM Query/Receive. This works reliably.
So probably getscu is not the appropriate script option?

Right, if you can’t use CGET then you need to be running the Slicer listener and then do the CFIND to get the list of instances and CMOVE to tell them to be transferred to Slicer’s listener. This can be done using dcmtk command line tools but that might not be cleanest.

It’s been been about a decade since we did all that code, but I’m pretty sure everything you need is exposed in python if you use the ctkDICOMQuery and ctkDICOMRetrieve classes. These are what the GUI uses under the hood and it looks like all the methods are exposed. The ctk doxygen is down, but if you look in the header files you’ll find what you need.

And you can see how they are used by the widget code - it should just be a matter of using the same API from python. Hope that helps!

1 Like

Thank you Steve.

In my DICOM retrieval extension

import ctk

query = ctk.ctkDICOMQuery

query.setCalledAETitle(“SLICER”)

query.setHost(“SRVXXX”)

query.setPort(1234)

query.setPreferCGET(False)

QMap<QString,QVariant> filters

filters[“ID”] = QString(studyID)

query.setFilters(filters)

results in

image

Am I missing anything here ?

Thanks & best regards
Rudolf

ctkDICOMQuery is a Qt class and calledAETitle is a property, which can be read/written in Python by its name (without set... prefix). See more information about how to translate C++ to Python here.

1 Like

Thanks Andras, but I am nowhere near it , sorry.

What I tried:

query = ctk.ctkDICOMQuery
query.CalledAETitle = "SLICER"
query.Host = "SRVxxx"
query.Port = nnnn
query.PreferCGET= False
filters = {'Name':'', 'Study':'', 'Series':'', 'ID':'', 'Modalities':'', 'StartDate':'', 'EndDate':''}  
filters["ID"] = studyID 
query.Filters = filters
query.query(???)

Is the problem related to

https://www.slicer.org/wiki/Documentation/Nightly/Developers/Python_scripting#Why_can.27t_I_access_my_C.2B.2B_Qt_class_from_python

maybe ?

I’v added a complete working example to the script repository: Documentation/Nightly/ScriptRepository - Slicer Wiki

I had to fix a few small issues in the ctkDICOMQuery class Python wrapping, so you can use this code with Slicer Preview Release that you download tomorrow or later.

2 Likes

Never ever saw such a great support anywhere.
At least I was on the right track somehow :slight_smile:

Thank you Andras!!
I will implement that and report back during the next days …

1 Like

It is nice to hear this. It would be great if you could write a few sentences about this good experience here:

Having a list of success stories could help with getting grant funding for Slicer.

Of course, Andras. Just done.

I just tried the above mentioned code after installation of
Slicer preview release 4.13.0 rev 29684 build 21-02-04

tempDb.open(’’)

throws the following error:

AttributeError: ctkDICOMDatabase has no attribute named ‘open’

Looked into CTK/ctkDICOMDatabase.h at master · commontk/CTK · GitHub

and changed

tempDb.open(’’)

to

tempDb.openDatabase(’’)

DICOM Query and Receive works great now !

1 Like

During the implementation of @lassoan 's script example I face the following problem:
The script works well with the public DICOM server, but does not work within the hospital setting. It returns a “failed” move operation upon retrieving the images.

The query itself seems to work fine and finds three CT series for a specific patient ID (which is correct)

All parameters are set exactly as I would do in the DICOM networking widget.

Is it possible that the function

dicomRetrieve.setMoveDestinationAETitle(“XXXXX”)

is not exposed yet?

I also tried

dicomRetrieve.moveDestinationAETitle = “XXXXX”

with no luck.

I need to set the retriever (“XXXXX”, the computer which makes the DICOM retrieve call) in the widget form field “Storage AETitle”, otherwise, the widget networking call would not work.

Second info:

I normally use it from the widget with CGET unchecked, but from the widget, it also works with CGET checked.

Debugging DIMSE networking is always a challenge. Both sides need to be configured just right, but it sounds like you are close.

Hmm, these appear be exposed correctly and that should match what’s happening in the GUI implementation.

In this case then you could try using the bool ctkDICOMRetrieve::getStudy(const QString& studyInstanceUID) or bool getSeries( const QString& studyInstanceUID, const QString& seriesInstanceUID ) methods? If the PACS side supports it then this is less tricky to configure.

This widget call works with query and retrieve:

The next call works with query, but not with receive (I only changed the Storage AETitle to a wrong parameter):

image

I get only a short popup and no DICOM download.

So retrieve works from the widget and setting the Storage AE Title correctly is crucial.

Then I run:

# Query
dicomQuery = ctk.ctkDICOMQuery()
dicomQuery.callingAETitle = "CKL1757"
dicomQuery.calledAETitle = "GPACS"
dicomQuery.host = "S078"
dicomQuery.port = 4001
dicomQuery.preferCGET = False
dicomQuery.filters = {'ID':'xxxxxxxxxx', 'Modalities':'CT'}    


# temporary in-memory database for storing query results
tempDb = ctk.ctkDICOMDatabase()
tempDb.openDatabase('')
dicomQuery.query(tempDb)

# Retrieve
dicomRetrieve = ctk.ctkDICOMRetrieve()
dicomRetrieve.callingAETitle = dicomQuery.callingAETitle
dicomRetrieve.calledAETitle = dicomQuery.calledAETitle
dicomRetrieve.host = dicomQuery.host
dicomRetrieve.port = dicomQuery.port
dicomRetrieve.moveDestinationAETitle = "CKL1757"
dicomRetrieve.setDatabase(slicer.dicomDatabase)


for study in dicomQuery.studyInstanceUIDQueried:
    print(f"ctkDICOMRetrieveTest: Retrieving >{study}<")
    slicer.app.processEvents()
    if dicomQuery.preferCGET:
        print(f"getting ...")
        success = dicomRetrieve.getStudy(study)
    else:
        print(f"moving ...")
        success = dicomRetrieve.moveStudy(study)
    print(f"  - {'success' if success else 'failed'}")

slicer.dicomDatabase.updateDisplayedFields()

and get three times "moving → “failed” for the three correctly detected CT datasets

Same happens if I set

dicomQuery.preferCGET = True

→ three times "getting → “failed” for the three correctly detected CT datasets

image

Any idea what could be the difference between the widget call (working) and the script call (failed) ?

The widget also sets how associations are handled, try to set that in your script:

Unfortunately, neither

dicomRetrieve.setKeepAssociationOpen(True)

nor

dicomRetrieve.keepAssociationOpen = True

changed anything …

The observation with the widget is: Takes quite a long time interval until the popup “Got a move request” is displayed (20 s), then it is again quite slow but realiable to retieve the data.

With the script, the query takes place almost instantly, the failed retrieval requests print messages are within 3 s.
I suspect a timing problem … if I could I would check logs on the server side, but I have no access myself and it is extremely difficult to get hold of the PACS guy