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:
- Entre no portal do Azure.
- Pesquise Recursos recentes e selecione sua instância de serviço DICOM.
- Copie a URL de Serviço do serviço DICOM.
- 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 .dcm
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 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) ouAccept: multipart/related; type="application/octet-stream"; transfer-syntax=*
ouAccept: 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.