Compartilhar via


Usar as APIs padrão do DICOMweb com Python

Esse artigo mostra como trabalhar com o serviço DICOMweb usando o Python e arquivos .dcm do DICOM® de exemplo.

Use esses arquivos de exemplo:

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

O nome do arquivo, studyUID, seriesUID e instanceUID dos arquivos DICOM de exemplo são:

Arquivo 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

Observação

Cada um desses arquivos representa uma única instância e faz parte do mesmo estudo. Além disso, o quadrado verde e o triângulo vermelho fazem parte da mesma série, enquanto o círculo azul está em uma série separada.

Pré-requisitos

Para usar as APIs Padrão DICOMweb, você deve ter uma instância do serviço DICOM implantada. Para obter mais informações, consulte Implantar o serviço DICOM usando o portal do Azure.

Depois de implantar uma instância do serviço DICOM, recupere a URL do serviço de aplicativo:

  1. Entre no portal do Azure.
  2. Pesquise Recursos recentes e selecione sua instância de serviço DICOM.
  3. Copie a URL de Serviço do serviço DICOM.
  4. Se você não tiver um token, consulte Obter token de acesso para o serviço DICOM usando a CLI do Azure.

Para esse código, você acessa um serviço de Visualização Pública do Azure. É importante que você não carregue nenhuma informação de saúde particular (PHI).

Trabalhar com o serviço DICOM

O DICOMweb Standard faz uso intenso de solicitações HTTP multipart/related combinadas com cabeçalhos de aceitação específicos do DICOM. Os desenvolvedores familiarizados com outras APIs baseadas em REST geralmente acham estranho trabalhar com o padrão DICOMweb. No entanto, depois que ele estiver funcionando, é fácil de usar. É preciso um pouco de familiaridade para começar.

Importar as bibliotecas do Python

Primeiro, importe as bibliotecas necessárias do Python.

Implementamos este exemplo usando a biblioteca síncrona requests. Para obter suporte assíncrono, considere usar httpx ou outra biblioteca assíncrona. Além disso, estamos importando duas funções de suporte de urllib3 para dar suporte ao trabalho com solicitações multipart/related.

Além disso, estamos importando DefaultAzureCredential para fazer logon no Azure e obter um token.

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

Configurar variáveis definidas pelo usuário

Substitua todos os valores de variáveis encapsulados em { } por seus próprios valores. Além disso, valide se todas as variáveis construídas estão corretas. Por exemplo, base_url é construído usando a URL de Serviço e, em seguida, acrescentado com a versão da API REST sendo usada. A URL de Serviço do serviço DICOM é: https://<workspacename-dicomservicename>.dicom.azurehealthcareapis.com. Você pode usar o portal do Azure para navegar até o serviço DICOM e obter sua URL de Serviço. Você também pode visitar o Controle de Versão da API para a Documentação do serviço DICOM para obter mais informações sobre controle de versão. Se você estiver usando uma URL personalizada, precisará substituir esse valor por sua própria.

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

Autenticar no Azure e obter um token

DefaultAzureCredential nos permite usar várias maneiras de obter tokens para fazer logon no serviço. Nesse exemplo, use o AzureCliCredential para obter um token para fazer logon no serviço. Existem outros provedores de credenciais, como ManagedIdentityCredential e EnvironmentCredential que você pode usar. Para usar o AzureCliCredential, você precisa entrar no Azure da CLI antes de executar esse código. Para obter mais informações, consulte Obter o token de acesso para o serviço DICOM usando a CLI do Azure. Como alternativa, copie e cole o token recuperado durante a entrada da CLI.

Observação

DefaultAzureCredential retorna vários objetos de Credencial diferentes. Fazemos referência ao AzureCliCredential como o 5º item da coleção retornada. Isso nem sempre é assim. Se não for, remova a marca de comentário na linha print(credential.credential). Isso listará todos os itens. Localize o índice correto, lembrando que o Python usa indexação baseada em zero.

Observação

Se você não tiver entrado no Azure usando a CLI, isso irá falhar. Você deve estar conectado ao Azure por meio da CLI para que isso 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}'

Criar métodos de suporte para dar suporte a multipart\related

As bibliotecas Requests (e a maioria das bibliotecas do Python) não funcionam com multipart\related de forma a dar suporte à DICOMweb. Devido a essas bibliotecas, devemos adicionar alguns métodos para dar suporte ao trabalho com arquivos DICOM.

encode_multipart_related usa um conjunto de campos (no caso DICOM, essas bibliotecas geralmente são arquivos dam da Parte 10) e um limite opcional definido pelo usuário. Esse linha retorna o corpo inteiro, juntamente com o content_type, que pode ser usado.

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

Criar uma sessão do requests.

Cria uma sessão do requests, chamada client que é usada para se comunicar com o serviço DICOM.

client = requests.session()

Verificar se a autenticação está configurada corretamente

Chame o ponto de extremidade da API de feed de alterações, que retorna um 200 se a autenticação for bem-sucedida.

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!')

Carregar instâncias DICOM (STOW)

Os exemplos a seguir realçam arquivos DICOM persistentes.

Armazenar instâncias usando multipart/related

Esse exemplo demonstra como carregar um único arquivo DICOM e usa o Python para pré-carregar o arquivo DICOM na memória como bytes. Quando uma matriz de arquivos é passada para o parâmetro encode_multipart_related dos campos, vários arquivos podem ser carregados em um único POST. Às vezes, ele é usado para carregar várias instâncias dentro de uma série ou estudo completo.

Detalhes:

  • Path: ../studies

  • Método: POST

  • Cabeçalhos:

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

    • Content-Type: application/dicom para cada arquivo carregado, separado por um valor de limite

Algumas linguagens de programação e ferramentas se comportam de forma diferente. Por exemplo, alguns exigem que você defina seu próprio limite. Para esses idiomas e ferramentas, talvez seja necessário usar um cabeçalho tipo de conteúdo ligeiramente modificado. Esses idiomas e ferramentas podem ser usados com êxito.

  • 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)

Armazenar instâncias para um estudo específico

Esse exemplo demonstra como carregar vários arquivos DICOM no estudo especificado. Ele usa o Python para pré-carregar o arquivo DICOM na memória como bytes.

Quando uma matriz de arquivos é passada para o parâmetro encode_multipart_related dos campos, vários arquivos podem ser carregados em um único POST. Às vezes, ele é usado para carregar uma série ou estudo completo.

Detalhes:

  • Path: ../studies/{study}
  • Método: POST
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Content-Type: multipart/related; type="application/dicom"
    • Authorization: Bearer $token"
  • Corpo:
    • Content-Type: application/dicom para cada arquivo carregado, separado por um valor de limite

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)

Armazenar uma instância única (não padrão)

O exemplo de código a seguir demonstra como carregar um único arquivo DICOM. Ele é um ponto de extremidade de API não padrão que simplifica o carregamento de um único arquivo como bytes binários enviados no corpo de uma solicitação

Detalhes:

  • Path: ../studies
  • Método: POST
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Content-Type: application/dicom
    • Authorization: Bearer $token"
  • Corpo:
    • Contém um único arquivo DICOM como bytes binários.
#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

Recuperar instâncias de DICOM (WADO)

Os exemplos a seguir realçam a recuperação de instâncias DICOM.

Recuperar todas as instâncias em um estudo

Este exemplo recupera todas as instâncias em um único estudo.

Detalhes:

  • Path: ../studies/{study}
  • Método: GET
  • Cabeçalhos:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Todos os três arquivos DCM carregados anteriormente fazem parte do mesmo estudo, portanto, a resposta deve retornar todas as três instâncias. Valide se a resposta tem um código de status de OK e se todas as três instâncias são retornadas.

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)

Usar as instâncias recuperadas

As instâncias são recuperadas como bytes binários. Você pode executar um loop pelos itens retornados e converter os bytes em um arquivo que pydicom possa ler da seguinte maneira.

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)

Recuperar metadados de todas as instâncias no estudo

Essa solicitação recupera os metadados de todas as instâncias em um único estudo.

Detalhes:

  • Path: ../studies/{study}/metadata
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Todos os três arquivos .dcmque carregamos anteriormente fazem parte do mesmo estudo, portanto, a resposta deve retornar os metadados para todas as três instâncias. Valide se a resposta tem um código de status OK e se todos os metadados são retornados.

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

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

Recuperar todas as instâncias em uma série

Essa solicitação recupera todas as instâncias em uma única série.

Detalhes:

  • Path: ../studies/{study}/series/{series}
  • Método: GET
  • Cabeçalhos:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Essa série tem duas instâncias (triângulo verde-quadrado e vermelho), portanto, a resposta deve retornar a duas instâncias. Valide se a resposta tem um código de status de OK e se ambas as instâncias são retornadas.

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)

Recuperar metadados de todas as instâncias da série

Essa solicitação recupera os metadados de todas as instâncias em uma única série.

Detalhes:

  • Path: ../studies/{study}/series/{series}/metadata
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Essa série tem duas instâncias (quadrado verde e triângulo vermelho), portanto, a resposta deve retornar para ambas as instâncias. Valide se a resposta tem um código de status de OK e se os metadados das duas instâncias são retornadas.

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)

Recuperar uma única instância em uma série de um estudo

Essa solicitação recupera uma única instância.

Detalhes:

  • Path: ../studies/{study}/series{series}/instances/{instance}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom; transfer-syntax=*
    • Authorization: Bearer $token"

Esse exemplo de código só deve retornar o triângulo vermelho da instância. Valide se a resposta tem um código de status de OK e se a instância é retornada.

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)

Recuperar metadados de uma única instância em uma série de um estudo

Essa solicitação recupera os metadados de uma única instância em um único estudo e série.

Detalhes:

  • Path: ../studies/{study}/series/{series}/instances/{instance}/metadata
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Esse exemplo de código só deve retornar os metadados para o triângulo vermelho da instância. Valide se a resposta tem um código de status de OK e se os metadados são retornados.

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)

Recuperar um ou mais quadros de uma única instância

Essa solicitação recupera um ou mais quadros de uma única instância.

Detalhes:

  • Path: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3
  • Método: GET
  • Cabeçalhos:
    • Authorization: Bearer $token"
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1 (Padrão) ou
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=* ou
    • Accept: multipart/related; type="application/octet-stream";

Esse exemplo de código deve retornar o único quadro do triângulo vermelho. Valide se a resposta tem um código de status de OK e se o quadro é retornado.

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)

Query DICOM (QIDO)

Nos exemplos a seguir, pesquisamos itens usando seus identificadores exclusivos. Você também pode pesquisar outros atributos, como PatientName.

Consulte a Instrução de Conformidade DICOM para atributos DICOM com suporte.

Pesquisar estudos

Essa solicitação procura por um ou mais estudos por atributos DICOM.

Detalhes:

  • Path: ../studies?StudyInstanceUID={study}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui um estudo e se o código de resposta é 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)

Pesquisar séries

Essa solicitação pesquisa uma ou mais séries por atributos DICOM.

Detalhes:

  • Path: ../series?SeriesInstanceUID={series}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui uma série e se o código de resposta é 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)

Pesquisar séries em um estudo

Essa solicitação procura uma ou mais séries em um único estudo por atributos DICOM.

Detalhes:

  • Path: ../studies/{study}/series?SeriesInstanceUID={series}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui uma série e se o código de resposta é 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)

Procurar instâncias

Essa solicitação procura uma ou mais instâncias por atributos DICOM.

Detalhes:

  • Path: ../instances?SOPInstanceUID={instance}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui uma instância e se o código de resposta é 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)

Pesquisar instâncias em um estudo

Essa solicitação procura uma ou mais instâncias em um único estudo por atributos DICOM.

Detalhes:

  • Path: ../studies/{study}/instances?SOPInstanceUID={instance}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui uma instância e se o código de resposta é 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)

Pesquisar instâncias em um estudo e série

Essa solicitação procura uma ou mais instâncias em um único estudo e uma única série por atributos DICOM.

Detalhes:

  • Path: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance}
  • Método: GET
  • Cabeçalhos:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Valide se a resposta inclui uma instância e se o código de resposta é 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)

Excluir DICOM

Observação

A exclusão não faz parte do padrão DICOM, mas foi adicionada por conveniência.

Um código de resposta 204 é retornado quando a exclusão é bem-sucedida. Um código de resposta 404 será retornado se os itens nunca existiram ou já tiverem sido excluídos.

Excluir uma instância específica em um estudo e uma série

Essa solicitação exclui uma única instância em um único estudo e uma única série.

Detalhes:

  • Path: ../studies/{study}/series/{series}/instances/{instance}
  • Método: DELETE
  • Cabeçalhos:
    • Authorization: Bearer $token

Essa solicitação exclui a instância de triângulo vermelho do servidor. Se funcionar, o código de status de resposta não terá conteúdo.

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

Excluir uma série específica em um estudo

Essa solicitação exclui uma única série (e todas as instâncias filho) em um único estudo.

Detalhes:

  • Path: ../studies/{study}/series/{series}
  • Método: DELETE
  • Cabeçalhos:
    • Authorization: Bearer $token

Esse exemplo de código exclui a instância verde-quadrada do servidor (é o único elemento restante na série). Se funcionar, o código de status de resposta não excluirá o conteúdo.

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

Excluir um estudo específico

Essa solicitação exclui um único estudo (e todas as séries e instâncias filho).

Detalhes:

  • Path: ../studies/{study}
  • Método: DELETE
  • Cabeçalhos:
    • Authorization: Bearer $token
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

Observação

DICOM® é a marca registrada da National Electrical Manufacturers Association para suas publicações de padrões relacionados às comunicações digitais de informações médicas.