Compartir a través de


Uso de la API estándar de DICOMweb con Python

En este artículo se muestra cómo trabajar con el servicio DICOMweb mediante Python y archivos .dcm de DICOM® de ejemplo.

Use estos archivos de ejemplo:

  • blue-circle.dcm
  • dicom-metadata.csv
  • green-square.dcm
  • red-triangle.dcm

El nombre de archivo, studyUID, seriesUID e instanceUID de los archivos DICOM de ejemplo son:

Archivo StudyUID SeriesUID InstanceUID
green-square.dcm 1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420 1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652 1.2.826.0.1.3680043.8.498.12714725698140337137334606354172323212
red-triangle.dcm 1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420 1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652 1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395
blue-circle.dcm 1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420 1.2.826.0.1.3680043.8.498.77033797676425927098669402985243398207 1.2.826.0.1.3680043.8.498.13273713909719068980354078852867170114

Nota:

Cada uno de estos archivos representa una sola instancia y forma parte del mismo estudio. Además, el cuadrado verde y el triángulo rojo forman parte de la misma serie, mientras que el círculo azul está en una serie independiente.

Requisitos previos

Para usar las API estándar de DICOMweb, debe tener una instancia del servicio DICOM implementado. Para más información, consulte Implementación del servicio DICOM mediante Azure Portal.

Después de implementar una instancia del servicio DICOM, recupere la dirección URL de App Service:

  1. Inicie sesión en Azure Portal.
  2. Busque Recursos recientes y seleccione la instancia del servicio DICOM.
  3. Copie la dirección URL del servicio del servicio DICOM.
  4. Si no tiene un token, consulte Obtención del token de acceso para el servicio DICOM mediante la CLI de Azure.

Para este código, accederá a un servicio de Azure en versión preliminar pública. Es importante que no cargue ninguna información privada sobre salud (PHI).

Uso del servicio DICOM

El estándar DICOMweb hace un uso intensivo de solicitudes HTTP multipart/related combinadas con encabezados de aceptación específicos de DICOM. Los desarrolladores familiarizados con otras API basadas en REST suelen encontrar muy complejo el estándar DICOMweb. Sin embargo, una vez en funcionamiento, es fácil de usar. Solo es necesario acostumbrarse al principio.

Importación de las bibliotecas de Python

En primer lugar, importe las bibliotecas de Python necesarias.

Vamos a implementar este ejemplo mediante la biblioteca requests sincrónica. Para obtener compatibilidad asincrónica, considere la posibilidad de usar httpx u otra biblioteca asincrónica. Además, se van a importar dos funciones auxiliares desde urllib3 para que admita el uso de solicitudes multipart/related.

Además, vamos a importar DefaultAzureCredential para iniciar sesión en Azure y obtener un token.

import requests
import pydicom
from pathlib import Path
from urllib3.filepost import encode_multipart_formdata, choose_boundary
from azure.identity import DefaultAzureCredential

Configuración de variables definidas por el usuario

Reemplace todos los valores de variable que están entre { } por sus propios valores. Además, compruebe que las variables creadas son correctas. Por ejemplo, base_url se construye mediante la dirección URL del servicio y, a continuación, se anexa la versión de la API REST que se usa. La dirección URL de servicio de DICOM es: https://<workspacename-dicomservicename>.dicom.azurehealthcareapis.com. Puede usar Azure Portal para ir al servicio DICOM y obtener la dirección URL del servicio. Para más información sobre el control de versiones, consulte la documentación sobre el control de versiones de API para el servicio DICOM. Si usa una dirección URL personalizada, debe reemplazar ese valor por el suyo propio.

dicom_service_name = "{server-name}"
path_to_dicoms_dir = "{path to the folder that includes green-square.dcm and other dcm files}"

base_url = f"{Service URL}/v{version}"

study_uid = "1.2.826.0.1.3680043.8.498.13230779778012324449356534479549187420"; #StudyInstanceUID for all 3 examples
series_uid = "1.2.826.0.1.3680043.8.498.45787841905473114233124723359129632652"; #SeriesInstanceUID for green-square and red-triangle
instance_uid = "1.2.826.0.1.3680043.8.498.47359123102728459884412887463296905395"; #SOPInstanceUID for red-triangle

Autenticación en Azure y obtención de un token

DefaultAzureCredential nos permite usar varias maneras de obtener tokens para iniciar sesión en el servicio. En este ejemplo, use AzureCliCredential para obtener un token para iniciar sesión en el servicio. Hay otros proveedores de credenciales, como ManagedIdentityCredential y EnvironmentCredential que puede usar. Para usar AzureCliCredential, debe iniciar sesión en Azure desde la CLI antes de ejecutar este código. Para más información, consulte Obtención del token de acceso para el servicio DICOM mediante la CLI de Azure. Como alternativa, copie y pegue el token recuperado al iniciar sesión desde la CLI.

Nota:

DefaultAzureCredential devuelve varios objetos de credencial diferentes. Hacemos referencia a la AzureCliCredential como el 5º elemento de la colección devuelta. Esto no siempre puede ser el caso. Si no es así, quite la marca de comentario de la línea print(credential.credential). Esto mostrará todos los elementos. Busque el índice correcto y recuerde que Python usa la indexación basada en cero.

Nota:

Si no ha iniciado sesión en Azure mediante la CLI, se producirá un error. Debe iniciar sesión en Azure desde la CLI para que funcione.

from azure.identity import DefaultAzureCredential
credential = DefaultAzureCredential()

#print(credential.credentials) # this can be used to find the index of the AzureCliCredential
token = credential.credentials[4].get_token('https://dicom.healthcareapis.azure.com')
bearer_token = f'Bearer {token.token}'

Creación de métodos auxiliares que admitan multipart\related

Las bibliotecas de Requests (y la mayoría de las bibliotecas de Python) no funcionan con multipart\related de una manera que admita DICOMweb. Debido a estas bibliotecas, debemos agregar algunos métodos que admitan el trabajo con archivos DICOM.

encode_multipart_related toma un conjunto de campos (en el caso de DICOM, estas bibliotecas suelen ser archivos dam de parte 10) y un límite opcional definido por el usuario. Devuelve el cuerpo completo, junto con el content_type, que se puede usar.

def encode_multipart_related(fields, boundary=None):
    if boundary is None:
        boundary = choose_boundary()

    body, _ = encode_multipart_formdata(fields, boundary)
    content_type = str('multipart/related; boundary=%s' % boundary)

    return body, content_type

Creación de una sesión de requests

Crea una sesión de requests, denominada client que se usa para comunicarse con el servicio DICOM.

client = requests.session()

Comprobación de que la autenticación está configurada correctamente

Llame al punto de conexión de la API de la fuente de cambios, la cual devolverá 200 si la autenticación es correcta.

headers = {"Authorization":bearer_token}
url= f'{base_url}/changefeed'

response = client.get(url,headers=headers)
if (response.status_code != 200):
    print('Error! Likely not authenticated!')

Carga de instancias DICOM (STOW)

En los ejemplos siguientes se resalta la conservación de archivos DICOM.

Almacenamiento de instancias mediante multipart/related

En este ejemplo se muestra cómo cargar un único archivo DICOM y usa Python para cargar previamente el archivo DICOM en la memoria como bytes. Si se pasa una matriz de archivos al parámetro fields encode_multipart_related, se pueden cargar varios archivos en una única ejecución de POST. A veces se usa para cargar varias instancias dentro de una serie completa o un estudio.

Detalles:

  • Ruta de acceso: ../studies

  • Método: POST

  • Encabezados:

    • Accept: application/dicom+json
    • Content-Type: multipart/related; type="application/dicom"
    • Authorization: Bearer $token"
  • Cuerpo:

    • Content-Type: application/dicom para cada archivo cargado, separado por un valor de límite

Algunos lenguajes de programación y herramientas se comportan de forma diferente. Por ejemplo, algunos requieren que defina su propio límite. Para esos lenguajes y herramientas, es posible que tenga que usar un encabezado Content-Type ligeramente modificado. Estos lenguajes y herramientas se pueden usar correctamente.

  • Content-Type: multipart/related; type="application/dicom"; boundary=ABCD1234
  • Content-Type: multipart/related; boundary=ABCD1234
  • Content-Type: multipart/related
#upload blue-circle.dcm
filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm')

# Read through file and load bytes into memory 
with open(filepath,'rb') as reader:
    rawfile = reader.read()
files = {'file': ('dicomfile', rawfile, 'application/dicom')}

#encode as multipart_related
body, content_type = encode_multipart_related(fields = files)

headers = {'Accept':'application/dicom+json', "Content-Type":content_type, "Authorization":bearer_token}

url = f'{base_url}/studies'
response = client.post(url, body, headers=headers, verify=False)

Almacena instancias para un estudio específico.

En este ejemplo se muestra cómo cargar varios archivos DICOM en el estudio especificado. Usa Python para cargar previamente el archivo DICOM en la memoria como bytes.

Si se pasa una matriz de archivos al parámetro fields encode_multipart_related, se pueden cargar varios archivos en una única ejecución de POST. A veces se usa para cargar una serie completa o un estudio.

Detalles:

  • Ruta de acceso: ../studies/{study}
  • Método: POST
  • Encabezados:
    • Aceptar: application/dicom+json
    • Content-Type: multipart/related; type="application/dicom"
    • Authorization: Bearer $token"
  • Cuerpo:
    • Content-Type: application/dicom para cada archivo cargado, separado por un valor de límite

filepath_red = Path(path_to_dicoms_dir).joinpath('red-triangle.dcm')
filepath_green = Path(path_to_dicoms_dir).joinpath('green-square.dcm')

# Open up and read through file and load bytes into memory 
with open(filepath_red,'rb') as reader:
    rawfile_red = reader.read()
with open(filepath_green,'rb') as reader:
    rawfile_green = reader.read()  
       
files = {'file_red': ('dicomfile', rawfile_red, 'application/dicom'),
         'file_green': ('dicomfile', rawfile_green, 'application/dicom')}

#encode as multipart_related
body, content_type = encode_multipart_related(fields = files)

headers = {'Accept':'application/dicom+json', "Content-Type":content_type, "Authorization":bearer_token}

url = f'{base_url}/studies'
response = client.post(url, body, headers=headers, verify=False)

Almacenamiento de instancia única (no estándar)

En el siguiente ejemplo de código se muestra cómo cargar un único archivo DICOM. Es un punto de conexión de API no estándar que simplifica la carga de un único archivo como bytes binarios enviados en el cuerpo de una solicitud.

Detalles:

  • Ruta de acceso: ../studies
  • Método: POST
  • Encabezados:
    • Aceptar: application/dicom+json
    • Content-Type: application/dicom
    • Authorization: Bearer $token"
  • Cuerpo:
    • Contiene un único archivo DICOM como bytes binarios.
#upload blue-circle.dcm
filepath = Path(path_to_dicoms_dir).joinpath('blue-circle.dcm')

# Open up and read through file and load bytes into memory 
with open(filepath,'rb') as reader:
    body = reader.read()

headers = {'Accept':'application/dicom+json', 'Content-Type':'application/dicom', "Authorization":bearer_token}

url = f'{base_url}/studies'
response = client.post(url, body, headers=headers, verify=False)
response  # response should be a 409 Conflict if the file was already uploaded in the above request

Recuperación de las instancias DICOM (WADO)

En los ejemplos siguientes se resalta la recuperación de instancias DICOM.

Recuperación de todas las instancias de un estudio

En este ejemplo se recuperan todas las instancias de un único estudio.

Detalles:

  • Ruta de acceso: ../studies/{study}
  • Método: GET
  • Encabezados:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Los tres archivos dcm que se cargaron anteriormente forman parte del mismo estudio, por lo que la respuesta debe devolver las tres instancias. Compruebe que la respuesta tiene un código de estado OK y que se devuelven las tres instancias.

url = f'{base_url}/studies/{study_uid}'
headers = {'Accept':'multipart/related; type="application/dicom"; transfer-syntax=*', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Uso de las instancias recuperadas

Las instancias se recuperan como bytes binarios. Puede recorrer en bucle los elementos devueltos y convertir los bytes en un archivo que pydicom puede leer como se indica a continuación.

import requests_toolbelt as tb
from io import BytesIO

mpd = tb.MultipartDecoder.from_response(response)
for part in mpd.parts:
    # Note that the headers are returned as binary!
    print(part.headers[b'content-type'])
    
    # You can convert the binary body (of each part) into a pydicom DataSet
    #   And get direct access to the various underlying fields
    dcm = pydicom.dcmread(BytesIO(part.content))
    print(dcm.PatientName)
    print(dcm.SOPInstanceUID)

Recuperación de metadatos de todas las instancias del estudio

Esta solicitud recupera los metadatos de todas las instancias de un único estudio.

Detalles:

  • Ruta de acceso: ../studies/{study}/metadata
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Los tres archivos .dcm que se cargaron anteriormente forman parte del mismo estudio, por lo que la respuesta debe devolver los metadatos de las tres instancias. Compruebe que la respuesta tiene un código de estado OK y que se devuelven todos los metadatos.

url = f'{base_url}/studies/{study_uid}/metadata'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Recuperación de todas las instancias de una serie

En esta solicitud se recuperan todas las instancias de una única serie.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}
  • Método: GET
  • Encabezados:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Esta serie tiene dos instancias (cuadrado verde y triángulo rojo), por lo que la respuesta debe devolver ambas instancias. Compruebe que la respuesta tiene un código de estado OK y que se devuelven ambas instancias.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}'
headers = {'Accept':'multipart/related; type="application/dicom"; transfer-syntax=*', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Recuperación de metadatos de todas las instancias de la serie

Esta solicitud recupera los metadatos de todas las instancias de una única serie.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}/metadata
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Esta serie tiene dos instancias (cuadrado verde y triángulo rojo), por lo que la respuesta debe devolver ambas instancias. Compruebe que la respuesta tiene un código de estado OK y que se devuelven los metadatos de ambas instancias.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}/metadata'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Recuperación de una sola instancia dentro de una serie de un estudio

Esta solicitud recupera una instancia única.

Detalles:

  • Ruta de acceso: ../studies/{study}/series{series}/instances/{instance}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom; transfer-syntax=*
    • Authorization: Bearer $token"

Este ejemplo de código solo debe devolver la instancia de triángulo rojo. Compruebe que la respuesta tiene un código de estado OK y que se devuelve la instancia.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}'
headers = {'Accept':'application/dicom; transfer-syntax=*', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Recuperación de los metadatos de una sola instancia dentro de una serie de un estudio

Esta solicitud recupera los metadatos de una sola instancia dentro de un único estudio y una única serie.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}/instances/{instance}/metadata
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Este ejemplo de código solo debe devolver los metadatos de la instancia de triángulo rojo. Compruebe que la respuesta tiene un código de estado OK y que se devuelven los metadatos.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/metadata'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Recuperación de uno o varios marcos de una sola instancia

Esta solicitud recupera uno o varios marcos de una única instancia.

Detalles:

  • Ruta de acceso: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3
  • Método: GET
  • Encabezados:
    • Authorization: Bearer $token"
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1 (predeterminado) o
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=* o
    • Accept: multipart/related; type="application/octet-stream";

Este ejemplo de código debe devolver el único marco del triángulo rojo. Valide que la respuesta tenga un código de estado OK y que se devuelva el marco.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}/frames/1'
headers = {'Accept':'multipart/related; type="application/octet-stream"; transfer-syntax=*', "Authorization":bearer_token}

response = client.get(url, headers=headers) #, verify=False)

Consulta de DICOM (QIDO)

En los ejemplos siguientes, buscamos elementos mediante sus identificadores únicos. También puede buscar otros atributos, como PatientName.

Consulte la instrucción de conformidad DICOM para conocer los atributos DICOM admitidos.

Búsqueda de estudios

Esta solicitud busca uno o varios estudios por atributos DICOM.

Detalles:

  • Ruta de acceso: ../studies?StudyInstanceUID={study}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya un estudio y que el código de respuesta sea OK.

url = f'{base_url}/studies'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}
params = {'StudyInstanceUID':study_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Búsqueda de series

Esta solicitud busca una o varias series por atributos DICOM.

Detalles:

  • Ruta de acceso: ../series?SeriesInstanceUID={series}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya una serie y que el código de respuesta sea OK.

url = f'{base_url}/series'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}
params = {'SeriesInstanceUID':series_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Búsqueda de series en un estudio

Esta solicitud busca una o varias series dentro de un único estudio mediante atributos DICOM.

Detalles:

  • Ruta de acceso: ../studies/{study}/series?SeriesInstanceUID={series}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya una serie y que el código de respuesta sea OK.

url = f'{base_url}/studies/{study_uid}/series'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}
params = {'SeriesInstanceUID':series_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Búsqueda de instancias

Esta solicitud busca una o varias instancias por atributos DICOM.

Detalles:

  • Ruta de acceso: ../instances?SOPInstanceUID={instance}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya una instancia y que el código de respuesta sea OK.

url = f'{base_url}/instances'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}
params = {'SOPInstanceUID':instance_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Búsqueda de instancias en un estudio

Esta solicitud busca una o varias instancias dentro de un único estudio por atributos DICOM.

Detalles:

  • Ruta de acceso: ../studies/{study}/instances?SOPInstanceUID={instance}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya una instancia y que el código de respuesta sea OK.

url = f'{base_url}/studies/{study_uid}/instances'
headers = {'Accept':'application/dicom+json', "Authorization":bearer_token}
params = {'SOPInstanceUID':instance_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Búsqueda de instancias dentro de un estudio y serie

Esta solicitud busca una o varias instancias dentro de un único estudio y serie por atributos DICOM.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance}
  • Método: GET
  • Encabezados:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide que la respuesta incluya una instancia y que el código de respuesta sea OK.

url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances'
headers = {'Accept':'application/dicom+json'}
params = {'SOPInstanceUID':instance_uid}

response = client.get(url, headers=headers, params=params) #, verify=False)

Eliminación de DICOM

Nota:

La eliminación no forma parte del estándar DICOM, pero se ha agregado para mayor comodidad.

Se devuelve un código de respuesta 204 cuando la eliminación se realiza correctamente. Se devuelve un código de respuesta 404 si los elementos nunca existieron o ya se han eliminado.

Eliminación de una instancia específica de un estudio y una serie

Esta solicitud elimina una única instancia de un único estudio y una sola serie.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}/instances/{instance}
  • Método: DELETE
  • Encabezados:
    • Authorization: Bearer $token

Esta solicitud elimina la instancia de triángulo rojo del servidor. Si se realiza correctamente, el código de estado de respuesta no incluye contenido.

headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}/series/{series_uid}/instances/{instance_uid}'
response = client.delete(url, headers=headers) 

Eliminación de una serie específica de un estudio

Esta solicitud elimina una única serie (y todas las instancias secundarias) de un único estudio.

Detalles:

  • Ruta de acceso: ../studies/{study}/series/{series}
  • Método: DELETE
  • Encabezados:
    • Authorization: Bearer $token

Esta respuesta elimina la instancia del cuadrado verde (es el único elemento que queda en la serie) del servidor. Si se realiza correctamente, el código de estado de respuesta no elimina contenido.

headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}/series/{series_uid}'
response = client.delete(url, headers=headers) 

Eliminación de un estudio específico

Esta solicitud elimina un único estudio (y todas las series e instancias secundarias).

Detalles:

  • Ruta de acceso: ../studies/{study}
  • Método: DELETE
  • Encabezados:
    • Authorization: Bearer $token
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

Nota:

DICOM® es la marca registrada de la Asociación Nacional de Fabricantes Eléctricos para sus publicaciones de normas relacionadas con las comunicaciones digitales de información médica.