pydicom / pynetdicom

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

|coverage| |unit-tests| |type-hints| |docs| |black| |pypi-versions| |python-versions| |conda| |zenodo|

.. |coverage| image:: https://codecov.io/gh/pydicom/pynetdicom/branch/main/graph/badge.svg :target: https://codecov.io/gh/pydicom/pynetdicom

.. |unit-tests| image:: https://github.com/pydicom/pynetdicom/workflows/unit-tests/badge.svg :target: https://github.com/pydicom/pynetdicom/actions?query=workflow%3Aunit-tests

.. |type-hints| image:: https://github.com/pydicom/pynetdicom/workflows/type-hints/badge.svg :target: https://github.com/pydicom/pynetdicom/actions?query=workflow%3Atype-hints

.. |docs| image:: https://circleci.com/gh/pydicom/pynetdicom/tree/main.svg?style=shield :target: https://circleci.com/gh/pydicom/pynetdicom/tree/main

.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg :target: https://github.com/psf/black

.. |pypi-versions| image:: https://badge.fury.io/py/pynetdicom.svg :target: https://badge.fury.io/py/pynetdicom

.. |python-versions| image:: https://img.shields.io/pypi/pyversions/pynetdicom.svg :target: https://img.shields.io/pypi/pyversions/pynetdicom.svg

.. |conda| image:: https://img.shields.io/conda/vn/conda-forge/pynetdicom.svg :target: https://anaconda.org/conda-forge/pynetdicom

.. |zenodo| image:: https://zenodo.org/badge/DOI/10.5281/zenodo.3880767.svg :target: https://doi.org/10.5281/zenodo.3880767

pynetdicom

A Python implementation of the DICOM <https://www.dicomstandard.org>_ networking protocol, originally based on (legacy) pynetdicom <https://github.com/patmun/pynetdicom_legacy>_.

Description

DICOM <https://www.dicomstandard.org>_ is the international standard for medical images and related information. It defines the formats and communication protocols for media exchange in radiology, cardiology, radiotherapy and other medical domains.

pynetdicom is a pure Python package that implements the DICOM networking protocol. Working with pydicom <https://github.com/pydicom/pydicom>_, it allows the easy creation of DICOM Service Class Users (SCUs) and Service Class Providers (SCPs).

pynetdicom's main user class is AE <https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.ae.ApplicationEntity.html>_ and is used to represent a DICOM Application Entity. With it you can:

Once associated, the services available to the association can be used by sending DIMSE-C <https://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_9>_ and DIMSE-N <https://dicom.nema.org/medical/dicom/current/output/html/part07.html#chapter_10>_ messages.

Documentation

The pynetdicom tutorials <https://pydicom.github.io/pynetdicom/stable/tutorials/index.html>, user guide <https://pydicom.github.io/pynetdicom/stable/user/index.html>, code examples <https://pydicom.github.io/pynetdicom/stable/examples/index.html>, application <https://pydicom.github.io/pynetdicom/stable/apps/index.html> and API reference <https://pydicom.github.io/pynetdicom/stable/reference/index.html> documentation is available for the current release <https://pydicom.github.io/pynetdicom/> as well as the development version <https://pydicom.github.io/pynetdicom/dev>_.

Installation

Dependencies

`pydicom <https://github.com/pydicom/pydicom>`_

Installing current release

Using pip:

.. code-block:: sh

pip install -U pynetdicom

Using conda:

.. code-block:: sh

conda install -c conda-forge pynetdicom

For more detailed instructions, including how to install the current development version, please see the installation guide <https://pydicom.github.io/pynetdicom/stable/tutorials/installation.html>_.

Supported DIMSE Services

SCU Services


When the AE is acting as an SCU and an association has been established with a
peer SCP, the following DIMSE-C and -N services are available:

.. _assoc: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html
.. _echo: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_c_echo
.. _find: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_c_find
.. _c_get: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_c_get
.. _move: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_c_move
.. _store: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_c_store
.. _action: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_action
.. _create: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_create
.. _delete: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_delete
.. _er: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_event_report
.. _n_get: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_get
.. _set: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html#pynetdicom.association.Association.send_n_set

+----------------+----------------------------------------------------------------------------------------+
| DIMSE service  | `Association <assoc_>`_ method                                                         |
+================+========================================================================================+
| C-ECHO         | `Association.send_c_echo() <echo_>`_                                                   |
+----------------+----------------------------------------------------------------------------------------+
| C-FIND         | `Association.send_c_find(dataset, query_model) <find_>`_                               |
+----------------+----------------------------------------------------------------------------------------+
| C-GET          | `Association.send_c_get(dataset, query_model) <c_get_>`_                               |
+----------------+----------------------------------------------------------------------------------------+
| C-MOVE         | `Association.send_c_move(dataset, move_aet, query_model) <move_>`_                     |
+----------------+----------------------------------------------------------------------------------------+
| C-STORE        | `Association.send_c_store(dataset) <store_>`_                                          |
+----------------+----------------------------------------------------------------------------------------+
| N-ACTION       | `Association.send_n_action(dataset, action_type, class_uid, instance_uid) <action_>`_  |
+----------------+----------------------------------------------------------------------------------------+
| N-CREATE       | `Association.send_n_create(dataset, class_uid, instance_uid) <create_>`_               |
+----------------+----------------------------------------------------------------------------------------+
| N-DELETE       | `Association.send_n_delete(class_uid, instance_uid) <delete_>`_                        |
+----------------+----------------------------------------------------------------------------------------+
| N-EVENT-REPORT | `Association.send_n_event_report(dataset, event_type, class_uid, instance_uid) <er_>`_ |
+----------------+----------------------------------------------------------------------------------------+
| N-GET          | `Association.send_n_get(identifier_list, class_uid, instance_uid) <n_get_>`_           |
+----------------+----------------------------------------------------------------------------------------+
| N-SET          | `Association.send_n_set(dataset, class_uid, instance_uid) <set_>`_                     |
+----------------+----------------------------------------------------------------------------------------+

Where *dataset* is a pydicom
`Dataset <https://pydicom.github.io/pydicom/stable/ref_guide.html#dataset>`_
object, *query_model* is a UID string, *identifier_list* is a list of pydicom
`Tag <https://pydicom.github.io/pydicom/stable/api_ref.html#pydicom.tag.Tag>`_
objects, *event_type* and *action_type* are ints and *class_uid* and
*instance_uid* are UID strings. See the
`Association documentation <https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.association.Association.html>`_
for more information.

SCP Services

When the AE is acting as an SCP the following DIMSE-C and -N services are available to the peer once an association has been established:

.. _hecho: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_echo.html .. _hfind: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_find.html .. _hc_get: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_c_get.html .. _hmove: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_move.html .. _hstore: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_store.html .. _haction: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_action.html .. _hcreate: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_create.html .. _hdelete: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_delete.html .. _her: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_event_report.html .. _hn_get: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_n_get.html .. _hset: https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom._handlers.doc_handle_set.html

+----------------+----------------------------+---------------------------------+ | DIMSE service | Intervention Event | Handler documentation | +================+============================+=================================+ | C-ECHO | evt.EVT_C_ECHO | Handle C-ECHO <hecho_>_ | +----------------+----------------------------+---------------------------------+ | C-FIND | evt.EVT_C_FIND | Handle C-FIND <hfind_>_ | +----------------+----------------------------+---------------------------------+ | C-GET | evt.EVT_C_GET | Handle C-GET <hc_get_>_ | +----------------+----------------------------+---------------------------------+ | C-MOVE | evt.EVT_C_MOVE | Handle C-MOVE <hmove_>_ | +----------------+----------------------------+---------------------------------+ | C-STORE | evt.EVT_C_STORE | Handle C-STORE <hstore_>_ | +----------------+----------------------------+---------------------------------+ | N-ACTION | evt.EVT_N_ACTION | Handle N-ACTION <haction_>_ | +----------------+----------------------------+---------------------------------+ | N-CREATE | evt.EVT_N_CREATE | Handle N-CREATE <hcreate_>_ | +----------------+----------------------------+---------------------------------+ | N-DELETE | evt.EVT_N_DELETE | Handle N-DELETE <hdelete_>_ | +----------------+----------------------------+---------------------------------+ | N-EVENT-REPORT | evt.EVT_N_EVENT_REPORT | Handle N-EVENT-REPORT <her_>_ | +----------------+----------------------------+---------------------------------+ | N-GET | evt.EVT_N_GET | Handle N-GET <hn_get_>_ | +----------------+----------------------------+---------------------------------+ | N-SET | evt.EVT_N_SET | Handle N-SET <hset_>_ | +----------------+----------------------------+---------------------------------+

With the exception of the C-ECHO service, a user-defined callable function, handler, must be bound to the corresponding intervention event <https://pydicom.github.io/pynetdicom/stable/user/events#intervention-events>_ in order to complete a DIMSE service request. Events can be imported with from pynetdicom import evt and a handler can be bound to an event prior to starting an association through the evt_handlers keyword arguments in AE.start_server() <https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.ae.ApplicationEntity.html#pynetdicom.ae.ApplicationEntity.start_server> and AE.associate() <https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.ae.ApplicationEntity.html#pynetdicom.ae.ApplicationEntity.associate>.

When an event occurs the handler function is called and passed a single parameter, event, which is an Event <https://pydicom.github.io/pynetdicom/stable/reference/generated/pynetdicom.events.Event.html> object whose specific attributes are dependent on the type of event that occurred. Handlers bound to intervention events must return or yield certain values. See the handler documentation <https://pydicom.github.io/pynetdicom/stable/reference/events> for information on what attributes and properties are available in Event for each event type and the expected returns/yields for the corresponding handlers.

Applications

Some basic DICOM applications are included with pynetdicom:

Code Examples

More code examples <https://pydicom.github.io/pynetdicom/stable/examples/index.html>_ are available in the documentation.

Echo SCU

Send a C-ECHO request to a Verification SCP (at TCP/IP address
*addr*, listen port number *port*):

.. code-block:: python

        from pynetdicom import AE

        ae = AE(ae_title='MY_ECHO_SCU')
        # Verification SOP Class has a UID of 1.2.840.10008.1.1
        #   we can use the UID str directly when adding the requested
        #   presentation context
        ae.add_requested_context('1.2.840.10008.1.1')

        # Associate with a peer AE
        assoc = ae.associate(addr, port)

        if assoc.is_established:
            # Send a DIMSE C-ECHO request to the peer
            status = assoc.send_c_echo()

            # Print the response from the peer
            if status:
                print('C-ECHO Response: 0x{0:04x}'.format(status.Status))

            # Release the association
            assoc.release()

Echo SCP

Create a blocking Echo SCP on port 11112 (you may optionally bind a handler to the evt.EVT_C_ECHO event if you want to return something other than an 0x0000 Success status):

.. code-block:: python

    from pynetdicom import AE, VerificationPresentationContexts

    ae = AE(ae_title='MY_ECHO_SCP')
    # Or we can use the inbuilt VerificationPresentationContexts list,
    #   there's one for each of the supported Service Classes
    # In this case, we are supporting any requests to use Verification SOP
    #   Class in the association
    ae.supported_contexts = VerificationPresentationContexts

    # Start the SCP on (host, port) in blocking mode
    ae.start_server(("localhost", 11112), block=True)

Alternatively, you can start the SCP in non-blocking mode, which returns the running server instance. This can be useful when you want to run a Storage SCP and make C-MOVE requests within the same AE.

In the next example we'll create a non-blocking Verification SCP and bind a handler for the C-ECHO service request event evt.EVT_C_ECHO that logs the requestor's address and port number and the timestamp for the event.

.. code-block:: python

    import logging

    from pynetdicom import AE, evt, debug_logger
    from pynetdicom.sop_class import Verification

    # Setup logging to use the StreamHandler at the debug level
    debug_logger()

    ae = AE(ae_title='MY_ECHO_SCP')
    ae.add_supported_context(Verification)

    # Implement the EVT_C_ECHO handler
    def handle_echo(event, logger):
        """Handle a C-ECHO service request.

        Parameters
        ----------
        event : evt.Event
            The C-ECHO service request event, this parameter is always
            present.
        logger : logging.Logger
            The logger to use, this parameter is only present because we
            bound ``evt.EVT_C_ECHO`` using a 3-tuple.

        Returns
        -------
        int or pydicom.dataset.Dataset
            The status returned to the peer AE in the C-ECHO response.
            Must be a valid C-ECHO status value as either an ``int`` or a
            ``Dataset`` object containing an (0000,0900) *Status* element.
        """
        # Every *Event* includes `assoc` and `timestamp` attributes
        #   which are the *Association* instance the event occurred in
        #   and the *datetime.datetime* the event occurred at
        requestor = event.assoc.requestor
        timestamp = event.timestamp.strftime("%Y-%m-%d %H:%M:%S")
        msg = (
            "Received C-ECHO service request from ({}, {}) at {}"
            .format(requestor.address, requestor.port, timestamp)
        )
        logger.info(msg)

        # Return a *Success* status
        return 0x0000

    # By binding using a 3-tuple we can pass extra arguments to
    #   the handler
    handlers = [(evt.EVT_C_ECHO, handle_echo, [logging.getLogger('pynetdicom')])]

    # Start the SCP in non-blocking mode
    scp = ae.start_server(("localhost", 11112), block=False, evt_handlers=handlers)

    # Associate and send a C-ECHO request to our own Verification SCP
    ae.add_requested_context(Verification)
    assoc = ae.associate('localhost', 11112)
    if assoc.is_established:
        status = assoc.send_c_echo()
        assoc.release()

    # Shutdown the SCP
    scp.shutdown()

Storage SCU


Send the DICOM *CT Image Storage* dataset in *file-in.dcm* to a peer Storage
SCP (at TCP/IP address *addr*, listen port number *port*):

.. code-block:: python

        from pydicom import dcmread
        from pydicom.uid import ImplicitVRLittleEndian

        from pynetdicom import AE, VerificationPresentationContexts
        from pynetdicom.sop_class import CTImageStorage, MRImageStorage

        ae = AE(ae_title='MY_STORAGE_SCU')
        # We can also do the same thing with the requested contexts
        ae.requested_contexts = VerificationPresentationContexts
        # Or we can use inbuilt objects like CTImageStorage.
        # The requested presentation context's transfer syntaxes can also
        #   be specified using a str/UID or list of str/UIDs
        ae.add_requested_context(CTImageStorage,
                                 transfer_syntax=ImplicitVRLittleEndian)
        # Adding a presentation context with multiple transfer syntaxes
        ae.add_requested_context(MRImageStorage,
                                 transfer_syntax=[ImplicitVRLittleEndian,
                                                  '1.2.840.10008.1.2.1'])

        assoc = ae.associate(addr, port)
        if assoc.is_established:
            dataset = dcmread('file-in.dcm')
            # `status` is the response from the peer to the store request
            # but may be an empty pydicom Dataset if the peer timed out or
            # sent an invalid dataset.
            status = assoc.send_c_store(dataset)

            assoc.release()