Use APIs padrão DICOMweb com Python

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

Use estes arquivos de exemplo:

  • azul-círculo.dcm
  • dicom-metadados.csv
  • verde-quadrado.dcm
  • vermelho-triângulo.dcm

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

Ficheiro EstudoUID SérieUID InstânciaUID
verde-quadrado.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
vermelho-triângulo.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
azul-círculo.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 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 seu serviço de aplicativo:

  1. Inicie sessão no portal do Azure.
  2. Pesquise recursos recentes e selecione sua instância de serviço DICOM.
  3. Copie a URL 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 do Azure de Visualização Pública. É importante que não carregue nenhuma informação de saúde privada (PHI).

Trabalhar com o serviço DICOM

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

Importar as bibliotecas Python

Primeiro, importe as bibliotecas Python necessárias.

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

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 { } pelos seus próprios valores. Além disso, valide se todas as variáveis construídas estão corretas. Por exemplo, é construído usando a URL do Serviço e, em seguida, base_url anexado com a versão da API REST que está sendo usada. O URL do Serviço do seu 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 a documentação do serviço API Versioning for DICOM para obter mais informações sobre controle de versão. Se estiver a utilizar um URL personalizado, terá de substituir esse valor pelo seu próprio.

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 Permite-nos utilizar várias formas de obter tokens para iniciar sessão no serviço. Neste exemplo, use o AzureCliCredential para obter um token para fazer logon no serviço. Existem outros provedores de credenciais, como ManagedIdentityCredential e EnvironmentCredential que também são possíveis de usar. Para usar o AzureCliCredential, você precisa entrar no Azure a partir da CLI antes de executar esse código. Para obter mais informações, consulte Obter token de acesso para o serviço DICOM usando a CLI do Azure. Como alternativa, copie e cole o token recuperado ao entrar na CLI.

Nota

DefaultAzureCredential retorna vários objetos Credential diferentes. Referimo-nos ao AzureCliCredential como o 5º item da coleção devolvida. Isto pode não ser coerente. Em caso afirmativo, descomente a print(credential.credential) linha. Isso listará todos os itens. Encontre o índice correto, lembrando que o Python usa indexação baseada em zero.

Nota

Se você não tiver feito logon no Azure usando a CLI, isso falhará. Você deve estar conectado ao Azure a partir 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 multipart\related

As Requests bibliotecas (e a maioria das bibliotecas Python) não funcionam de multipart\related uma forma que suporte DICOMweb. Por causa dessas bibliotecas, devemos adicionar alguns métodos para suportar o 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. Ele retorna tanto 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 requests sessão

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

client = requests.session()

Verifique se a autenticação está configurada corretamente

Chame o ponto de extremidade da API changefeed, 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 destacam arquivos DICOM persistentes.

Armazenar instâncias usando multipart/related

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

Detalhes:

  • Percurso: .. /estudos

  • Método: POST

  • Cabeçalhos:

    • Aceitar: application/dicom+json
    • Tipo de conteúdo: multiparte/relacionado; type="aplicação/dicom"
    • Autorização: Portador $token"
  • Corpo:

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

Algumas linguagens e ferramentas de programação comportam-se 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 Content-Type ligeiramente modificado. Estas linguagens e ferramentas podem ser utilizadas com sucesso.

  • Tipo de conteúdo: multiparte/relacionado; type="aplicação/dicom"; limite=ABCD1234
  • Tipo de conteúdo: multiparte/relacionado; limite=ABCD1234
  • Tipo de conteúdo: 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

Este exemplo demonstra como carregar vários arquivos DICOM no estudo especificado. Ele usa um pouco de Python para pré-carregar o arquivo DICOM (como bytes) na memória.

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

Detalhes:

  • Percurso: .. /estudos/{estudo}
  • Método: POST
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Tipo de conteúdo: multiparte/relacionado; type="aplicação/dicom"
    • Autorização: Portador $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 instância única (não padrão)

O exemplo de código a seguir demonstra como carregar um único arquivo DICOM. É 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:

  • Percurso: .. /estudos
  • Método: POST
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Tipo de conteúdo: application/dicom
    • Autorização: Portador $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 DICOM (WADO)

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

Recuperar todas as instâncias dentro de um estudo

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

Detalhes:

  • Percurso: .. /estudos/{estudo}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: multiparte/relacionado; type="aplicação/dicom"; transferência-sintaxe=*
    • Autorização: Portador $token"

Todos os três arquivos dcm que carregamos 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 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)

Use as instâncias recuperadas

As instâncias são recuperadas como bytes binários. Você pode percorrer os itens retornados e converter os bytes em um arquivo que pydicom pode ler.

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 em estudo

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

Detalhes:

  • Percurso: .. /studies/{study}/metadados
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

Todos os .dcm três arquivos que 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 dentro de uma série

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

Detalhes:

  • Percurso: .. /estudos/{estudo}/série/{série}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: multiparte/relacionado; type="aplicação/dicom"; transferência-sintaxe=*
    • Autorização: Portador $token"

Esta série tem duas ocorrências (verde-quadrado e vermelho-triângulo), portanto, a resposta deve retornar ambas as instâncias. Valide se a resposta tem um código de status 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 em série

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

Detalhes:

  • Percurso: .. /studies/{study}/series/{series}/metadata
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

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

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 dentro de uma série de um estudo

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

Detalhes:

  • Percurso: .. /studies/{study}/series{series}/instances/{instance}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: aplicação/dicom; transferência-sintaxe=*
    • Autorização: Portador $token"

Este exemplo de código deve retornar apenas a instância red-triangle. Valide se a resposta tem um código de status 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 dentro de 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:

  • Percurso: .. /studies/{study}/series/{series}/instances/{instance}/metadados
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

Este 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 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:

  • Percurso: .. /studies/{study}/series{series}/instances/{instance}/frames/1,2,3
  • Método: GET
  • Cabeçalhos:
    • Autorização: Portador $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";

Este exemplo de código deve retornar o único quadro do triângulo vermelho. Valide se a resposta tem um código de status 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)

Consulta DICOM (QIDO)

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

Consulte o documento Declaração de Conformidade DICOM para obter os atributos DICOM suportados.

Pesquisa de estudos

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

Detalhes:

  • Percurso: .. /estudos? StudyInstanceUID={estudo}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

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

Pesquisa de séries

Esta solicitação procura uma ou mais séries por atributos DICOM.

Detalhes:

  • Percurso: .. /série? SeriesInstanceUID={série}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

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

Pesquisa de séries dentro de um estudo

Esta solicitação procura uma ou mais séries dentro de um único estudo por atributos DICOM.

Detalhes:

  • Percurso: .. /estudos/{estudo}/série? SeriesInstanceUID={série}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

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

Pesquisar instâncias

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

Detalhes:

  • Percurso: .. /instâncias? SOPInstanceUID={instância}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

Valide se a resposta inclui uma instância e se o código de resposta está 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 dentro de um estudo

Esta solicitação procura uma ou mais instâncias dentro de um único estudo por atributos DICOM.

Detalhes:

  • Percurso: .. /studies/{study}/instances? SOPInstanceUID={instância}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

Valide se a resposta inclui uma instância e se o código de resposta está 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 dentro de um estudo e série

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

Detalhes:

  • Percurso: .. /studies/{study}/series/{series}/instances? SOPInstanceUID={instância}
  • Método: GET
  • Cabeçalhos:
    • Aceitar: application/dicom+json
    • Autorização: Portador $token"

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

Nota

Delete não faz parte do padrão DICOM, mas foi adicionado por conveniência.

Um código de resposta 204 é retornado quando a exclusão é bem-sucedida. Um código de resposta 404 é retornado se o(s) item(ns) nunca existiu(s) ou já foram excluídos.

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

Esta solicitação exclui uma única instância dentro de um único estudo e uma única série.

Detalhes:

  • Percurso: .. /studies/{study}/series/{series}/instances/{instance}
  • Método: DELETE
  • Cabeçalhos:
    • Autorização: $token ao portador

Essa solicitação exclui a instância de triângulo vermelho do servidor. Se for bem-sucedido, o código de status da resposta não conterá 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 dentro de um estudo

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

Detalhes:

  • Percurso: .. /estudos/{estudo}/série/{série}
  • Método: DELETE
  • Cabeçalhos:
    • Autorização: $token ao portador

Este exemplo de código exclui a instância do quadrado verde (é o único elemento restante na série) do servidor. Se for bem-sucedido, o código de status da 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) 

Eliminar um estudo específico

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

Detalhes:

  • Percurso: .. /estudos/{estudo}
  • Método: DELETE
  • Cabeçalhos:
    • Autorização: $token ao portador
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

Nota

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