pydicom / pynetdicom

A Python implementation of the DICOM networking protocol
https://pydicom.github.io/pynetdicom
MIT License
513 stars 180 forks source link

REQ: Download a DICOM from a Remote PACS #1

Closed barchard closed 6 years ago

barchard commented 7 years ago

These are some great examples and tutorials. Do you have one for downloading a DICOM from a remote PACS? Thanks

scaramallion commented 7 years ago

If the remote PACS supports the Query/Retrieve service classes and includes the PatientRootQueryRetrievalInformationModelGet or StudyRootQueryRetrieveInformationModalGet or the PatientStudyOnlyQueryRetrieveInformationModelGet SOP classes in its association negotiation (you can check with the echoscu.py app in debug mode using the -d flag) then you can either try the getscu.py app, which needs (a lot of) work but might be enough on its own for you, or you can write your own solution following the outline given in getscu.py.

Remote PACS also usually require that a peer SCU has been authorised, so the first step is to ensure you can associate correctly (using echoscu.py for example).

The basic approach is to:

The remote PACS should then search its stored SOP instances for matching datasets and send them to the running AE via DIMSE C-STORE.

Take a look at the DICOM standard part 4, Annex C (and C.4.3.1 in particular for C-GET) which explains in more detail what the dataset used to match against should look like.

There's not much testing of the assoc.send_c_get()/send_c_find()/send_c_move() parts of the code at the moment so if you run into anything strange don't hesitate to let me know.

barchard commented 7 years ago

Thanks. I'll start digging in and let you know if I run into issues. How would you compare this project to the original pynetdicom project? The API looks more fluent (which is nice). Also, do you know anyway to get a PACs to return "all data fields" and not just the specified ones specified in a Dataset for a find query or is that just how DICOM works? Thanks

scaramallion commented 7 years ago

I think the main differences between pynetdicom and pynetdicom3 at the moment are:

Its still very much a work in progress though.

Looking at the DICOM standard part 7, Section 9.1.2.1.5, it says that the response to a C-FIND operation will include a Dataset with

[...] the same list of Attributes with values of these Attributes in a particular composite SOP Instance that matched.

So a C-FIND operation won't return Datasets containing the matching SOP Instance(s), which I think is what you mean by "all data fields", only one with the same attributes you used to match against. A C-GET will return the full SOP Instance(s) though (via a C-STORE operation).

barchard commented 7 years ago

Thanks. Just curious, are you available for contract/hire? Do you have a website to contact you directly? Thanks

scaramallion commented 7 years ago

Not at the moment, sorry. If my situation changes I'll let you know.

ManojSolanki commented 6 years ago

Hi @barchard ,

I am new to the python and wanted to download the DICOM from PACS.

Are you able to download the DICOM files from PACS server to your local system? If you have some demo code for downloading the DICOM and storing them in system path then please share or guide me for the same.

@scaramallion

I am trying with below code somehow like you guided for downloading DICOM files into the system(I am using ORTHANC as a PACS server):

from pynetdicom3 import AE
from pydicom import read_file
from pydicom import Dataset
from pynetdicom3 import AE
from pynetdicom3 import StorageSOPClassList
from pynetdicom3 import QueryRetrieveSOPClassList

# The Verification SOP Class has a UID of 1.2.840.10008.1.1

ae = AE(scu_sop_class=QueryRetrieveSOPClassList)

# Try and associate with the peer AE
#   Returns the Association thread
print('Requesting Association with the peer')
assoc = ae.associate("127.0.0.1", 4242)

if assoc.is_established:
    print('Association accepted by the peer')

    # Creat a new DICOM dataset with the attributes to match against
    #   In this case match any patient's name at the PATIENT query
    #   level. See PS3.4 Annex C.6 for the complete list of possible
    #   attributes and query levels.
    dataset = Dataset()
    dataset.PatientId = '*'
    dataset.QueryRetrieveLevel = "PATIENT"

    # Send a DIMSE C-FIND request to the peer
    #   query_model is the Query/Retrieve Information Model to use
    #   and is one of 'W', 'P', 'S', 'O'
    #       'W' - Modality Worklist (1.2.840.10008.5.1.4.31)
    #       'P' - Patient Root (1.2.840.10008.5.1.4.1.2.1.1)
    #       'S' - Study Root (1.2.840.10008.5.1.4.1.2.2.1)
    #       'O' - Patient/Study Only (1.2.840.10008.5.1.4.1.2.3.1)
    responses = assoc.send_c_get(dataset, query_model='P')

    for (status, dataset) in responses:
        # While status is pending we should get the matching datasets
        if status == 'Pending':
            print(dataset)
        elif status == 'Success':
            print('C-FIND finished, releasing the association')
        elif status == 'Cancel':
            print('C-FIND cancelled, releasing the association')
        elif status == 'Failure':
            print('C-FIND failed, releasing the association')

    # Release the association
    assoc.release()

----------Error which I am getting------------ Requesting Association with the peer Association accepted by the peer E: No accepted Presentation Context for: 'Patient Root Query/Retrieve Information Model - GET' E: Get SCU failed due to there being no accepted presentation context for the current dataset Traceback (most recent call last): File "/Applications/PyCharm CE.app/Contents/helpers/pydev/pydevd.py", line 1599, in globals = debugger.run(setup['file'], None, None, is_module) File "/Applications/PyCharm CE.app/Contents/helpers/pydev/pydevd.py", line 1026, in run pydev_imports.execfile(file, globals, locals) # execute the script File "/ProjectData/Eko.ai/PACSServerDemo/DemoVersion2.py", line 37, in for (status, dataset) in responses: File "/Library/Python/2.7/site-packages/pynetdicom3-0.1.0-py2.7.egg/pynetdicom3/association.py", line 1578, in send_c_get raise ValueError("No accepted Presentation Context for 'dataset'") ValueError: No accepted Presentation Context for 'dataset'

Process finished with exit code 1

Also please let me know where exactly I will get the DICOM files object and I can write it to certain OS path.

Thanks in advance.

scaramallion commented 6 years ago

First of all, if you set the logging to the debug level you can check the association negotiation logs to see if the presentation context you're using been accepted by the SCP. If it hasn't then you need to configure the SCP properly. This should fix the 'No accepted Presentation Context for dataset' exception.

Secondly, you need to implement the AE.on_c_store(dataset) callback in order to handle datasets received as a result of the C-GET Query/Retrieve request. Take a look at the storescp app for an example of how to do that. Oh, and add the SOP Class UIDs you're expecting to get back to the scp_sop_class parameter in the AE initialisation.

Also, the element is PatientID, not PatientId

ManojSolanki commented 6 years ago

Hi @scaramallion

According to your instructions, I continue to downloading DICOM from PACS but still not able to get what is the correct Presentation Context for ‘dataset’. I tried all UID’s from following URL:

http://dicom.nema.org/medical/dicom/2014c/output/chtml/part02/sect_F.4.2.2.4.html

Here is my code: ct_storage_uid = UID('1.2.840.10008.1.1') ae = AE(scu_sop_class=[ct_storage_uid])

I also used the different combination of “query_model”:

if assoc.is_established: print('Association accepted by the peer')

# Creat a new DICOM dataset with the attributes to match against
#   In this case match any patient's name at the PATIENT query
#   level. See PS3.4 Annex C.6 for the complete list of possible
#   attributes and query levels.
dataset = Dataset()
dataset.PatientID = '*'
dataset.QueryRetrieveLevel = "PATIENT"

# Send a DIMSE C-FIND request to the peer
#   query_model is the Query/Retrieve Information Model to use
#   and is one of 'W', 'P', 'S', 'O'
#       'W' - Modality Worklist (1.2.840.10008.5.1.4.31)
#       'P' - Patient Root (1.2.840.10008.5.1.4.1.2.1.1)
#       'S' - Study Root (1.2.840.10008.5.1.4.1.2.2.1)
#       'O' - Patient/Study Only (1.2.840.10008.5.1.4.1.2.3.1)
responses = assoc.send_c_find(dataset, query_model='O')

I am using “ORTHANC” server, please find the logs for the same: screen shot 2018-05-23 at 3 59 31 pm ,

Which UID I should use for the connections, I can share sample DICOM file as well if it requires knowing the UID.

Please help.

scaramallion commented 6 years ago

You need to use the Query/Retrieve SOP Class UIDs you want to use when sending DIMSE messages to the peer. The simplest way is to add QueryRetriveSOPClassList to the scu_sop_class parameter.

Please run apps/findscu/findscu.py with the -d flag and post the output.

findscu.py 127.0.0.1 4242 -P -d path/to/dataset.dcm

ManojSolanki commented 6 years ago

I am getting following error, after running findscu.py:

usage: findscu [options] peer port dcmfile-in findscu.py: error: the following arguments are required: peer, port, dcmfile-in

After that I have changed following parameters:

# Parameters
req_opts = parser.add_argument_group('Parameters')
req_opts.add_argument("localhost", help="hostname of DICOM peer", type=str)
req_opts.add_argument(4242, help="TCP/IP port number of peer", type=int)
req_opts.add_argument("/ProjectData/DicomDIR",
                      metavar="dcmfile-in",
                      help="DICOM query file(s)",
                      type=str) 

but still getting following: Traceback (most recent call last): File "findscu.py", line 111, in args = _setup_argparser() File "findscu.py", line 46, in _setup_argparser req_opts.add_argument(4242, help="TCP/IP port number of peer", type=int) File "/anaconda3/lib/python3.6/argparse.py", line 1313, in add_argument if not args or len(args) == 1 and args[0][0] not in chars: TypeError: 'int' object is not subscriptable

Is the third parameter will be the path of my DICOM file folder?

Still trying...

scaramallion commented 6 years ago

There was no reason for you to modify findscu.py. Please revert the changes you made then do:

findscu.py 127.0.0.1 4242 -P -d path/to/dataset.dcm

Where 127.0.0.1 is the IP address of the SCP 4242 is the port number of the SCP -P use Patient Root Query Model - FIND -d show debugging output path/to/dataset.dcm is the path to a DICOM dataset, not a directory (any file will do, this isn't used for anything at the moment).

ManojSolanki commented 6 years ago

Hi @scaramallion ,

Here is the output:

Mac-mini:findscu manoj$ python findscu.py 127.0.0.1 4242 -P -d /Users/manoj/Downloads/ttfm.dcm D: $findscu.py v0.1.1 2017-07-02 $ D: I: Requesting Association D: Request Parameters: D: ====================== BEGIN A-ASSOCIATE-RQ ===================== D: Our Implementation Class UID: 1.2.826.0.1.3680043.9.3811.0.9.1 D: Our Implementation Version Name: PYNETDICOM3_091 D: Application Context Name: 1.2.840.10008.3.1.1.1 D: Calling Application Name: FINDSCU
D: Called Application Name: ANY-SCP
D: Our Max PDU Receive Size: 16382 D: Presentation Contexts: D: Context ID: 1 (Proposed) D: Abstract Syntax: =Patient Root Query/Retrieve Information Model - FIND D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 3 (Proposed) D: Abstract Syntax: =Study Root Query/Retrieve Information Model - FIND D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 5 (Proposed) D: Abstract Syntax: =Patient/Study Only Query/Retrieve Information Model - FIND D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 7 (Proposed) D: Abstract Syntax: =Modality Worklist Information Model - FIND D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 9 (Proposed) D: Abstract Syntax: =Patient Root Query/Retrieve Information Model - MOVE D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 11 (Proposed) D: Abstract Syntax: =Study Root Query/Retrieve Information Model - MOVE D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 13 (Proposed) D: Abstract Syntax: =Patient/Study Only Query/Retrieve Information Model - MOVE D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 15 (Proposed) D: Abstract Syntax: =Patient Root Query/Retrieve Information Model - GET D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 17 (Proposed) D: Abstract Syntax: =Study Root Query/Retrieve Information Model - GET D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Context ID: 19 (Proposed) D: Abstract Syntax: =Patient/Study Only Query/Retrieve Information Model - GET D: Proposed SCP/SCU Role: None/None D: Proposed Transfer Syntax: D: =Explicit VR Little Endian D: Requested Extended Negotiation: None D: Requested Common Extended Negotiation: None D: Requested User Identity Negotiation: None D: ======================= END A-ASSOCIATE-RQ ====================== D: Constructing Associate RQ PDU D: PDU Type: Associate Accept, PDU Length: 453 + 6 bytes PDU header D: 02 00 00 00 01 c5 00 01 00 00 41 4e 59 2d 53 43 D: 50 20 20 20 20 20 20 20 20 20 46 49 4e 44 53 43 D: 55 20 20 20 20 20 20 20 20 20 00 00 00 00 00 00 D: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 D: 00 00 00 00 00 00 00 00 00 00 10 00 00 15 31 2e D: 32 2e 38 34 30 2e 31 30 30 30 38 2e 33 2e 31 2e D: 31 2e 31 21 00 00 1b 01 00 00 00 40 00 00 13 31 D: 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 D: 2e 31 21 00 00 1b 03 00 00 00 40 00 00 13 31 2e D: 32 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 2e D: 31 21 00 00 19 05 00 03 00 40 00 00 11 31 2e 32 D: 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 21 00 D: 00 19 07 00 03 00 40 00 00 11 31 2e 32 2e 38 34 D: 30 2e 31 30 30 30 38 2e 31 2e 32 21 00 00 1b 09 D: 00 00 00 40 00 00 13 31 2e 32 2e 38 34 30 2e 31 D: 30 30 30 38 2e 31 2e 32 2e 31 21 00 00 1b 0b 00 D: 00 00 40 00 00 13 31 2e 32 2e 38 34 30 2e 31 30 D: 30 30 38 2e 31 2e 32 2e 31 21 00 00 19 0d 00 03 D: 00 40 00 00 11 31 2e 32 2e 38 34 30 2e 31 30 30 D: 30 38 2e 31 2e 32 21 00 00 19 0f 00 03 00 40 00 D: 00 11 31 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e D: 31 2e 32 21 00 00 19 11 00 03 00 40 00 00 11 31 D: 2e 32 2e 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 D: 21 00 00 19 13 00 03 00 40 00 00 11 31 2e 32 2e D: 38 34 30 2e 31 30 30 30 38 2e 31 2e 32 50 00 00 D: 3a 51 00 00 04 00 00 40 00 52 00 00 1b 31 2e 32 D: 2e 32 37 36 2e 30 2e 37 32 33 30 30 31 30 2e 33 D: 2e 30 2e 33 2e 36 2e 32 55 00 00 0f 4f 46 46 49 D: 53 5f 44 43 4d 54 4b 5f 33 36 32 D: Parsing an A-ASSOCIATE PDU D: Accept Parameters: D: ====================== BEGIN A-ASSOCIATE-AC ===================== D: Their Implementation Class UID: 1.2.276.0.7230010.3.0.3.6.2 D: Their Implementation Version Name: OFFIS_DCMTK_362 D: Application Context Name: 1.2.840.10008.3.1.1.1 D: Calling Application Name: FINDSCU
D: Called Application Name: ANY-SCP
D: Their Max PDU Receive Size: 16384 D: Presentation Contexts: D: Context ID: 1 (Accepted) D: Proposed SCP/SCU Role: Default D: Accepted SCP/SCU Role: Default D: Accepted Transfer Syntax: =Explicit VR Little Endian D: Context ID: 3 (Accepted) D: Proposed SCP/SCU Role: Default D: Accepted SCP/SCU Role: Default D: Accepted Transfer Syntax: =Explicit VR Little Endian D: Context ID: 5 (Abstract Syntax Not Supported) D: Context ID: 7 (Abstract Syntax Not Supported) D: Context ID: 9 (Accepted) D: Proposed SCP/SCU Role: Default D: Accepted SCP/SCU Role: Default D: Accepted Transfer Syntax: =Explicit VR Little Endian D: Context ID: 11 (Accepted) D: Proposed SCP/SCU Role: Default D: Accepted SCP/SCU Role: Default D: Accepted Transfer Syntax: =Explicit VR Little Endian D: Context ID: 13 (Abstract Syntax Not Supported) D: Context ID: 15 (Abstract Syntax Not Supported) D: Context ID: 17 (Abstract Syntax Not Supported) D: Context ID: 19 (Abstract Syntax Not Supported) D: Accepted Extended Negotiation: None D: Accepted Common Extended Negotiation: None D: Accepted Asynchronous Operations Window Negotiation: None D: User Identity Negotiation Response: None D: ======================= END A-ASSOCIATE-AC ====================== I: Association Accepted D: Checking input files I: Find SCU Request Identifiers: I: I: # DICOM Dataset I: (0008, 0052) Query/Retrieve Level CS: 'PATIENT' I: (0010, 0010) Patient's Name PN: '*' I: I: Sending Get Request: MsgID 1 D: ===================== OUTGOING DIMSE MESSAGE ==================== D: Message Type : C-FIND RQ D: Presentation Context ID : None D: Message ID : 1 D: Affected SOP Class UID : Patient Root Query/Retrieve Information Model - FIND D: Data Set : Present D: Priority : Low D: ======================= END DIMSE MESSAGE ======================= D: Abort Parameters: D: ========================== BEGIN A-ABORT ========================= D: Abort Source: DUL service-user D: Abort Reason: No reason given D: =========================== END A-ABORT ========================= E: Connection closed or timed-out E: Association Aborted

scaramallion commented 6 years ago
  1. The Orthanc SCP doesn't support any of the Query/Retrieve - GET abstract syntaxes (since presentation contexts 15, 17 and 19 are rejected). If you want to perform a C-GET operation then at least one of those syntaxes will need to be supported by Orthanc.
  2. The Orthanc SCP does support the Query/Retrieve - FIND syntax for Patient Root (since presentation context 1 is accepted) but is sending an A-ABORT after receiving a C-FIND message with that syntax, which I believe is linked to the following lines from the Orthanc log output you posted:

    Modality "PYNETDICOM" is not listed in the "DicomModalities" configuration option

Rejected Find request from remote DICOM modality with AET "PYNETDICOM" and hostname "127.0.0.1"

Both of which suggest to me that Orthanc SCP hasn't been configured properly. If you still have issues with pynetdicom once you've configured it properly please start a new issue about it (rather than posting in closed issues).

edmcdonagh commented 6 years ago

Also, should you not be using C-MOVE rather than C-GET?

https://www.medicalconnections.co.uk/kb/Basic_DICOM_Operations

varshit-i commented 4 years ago

Hi @ManojSolanki @scaramallion @barchard @edmcdonagh @cancan101

I need to download studies along with the DICOM images from the PACS servers, before, I was using Orthanc, I am getting the same issue while using C-GET and C-MOVE.

Can you please suggest the server on which I can test code and download the images using pynetdicom.

scaramallion commented 4 years ago

If you just need to test then try DCMTK's dcmqrscp. Its a little bit annoying to configure (your calling AE title needs to match one in the AETable section) but it's what I use to test pynetdicom against.

Essentially you get dcmqrscp configured and up and running, send some datasets to it using whatever Storage SCU you prefer and then you can send QR C-GET, C-FIND and C-MOVE requests to it.