Use APIs padrão DICOMweb com Python
Este artigo mostra como trabalhar com o serviço DICOMweb usando Python e exemplos .dcm arquivos DICOM®.
Use estes 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:
Ficheiro | EstudoUID | SérieUID | InstânciaUID |
---|---|---|---|
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 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:
- Inicie sessão no portal do Azure.
- Pesquise recursos recentes e selecione sua instância de serviço DICOM.
- Copie a URL 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 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 você pode 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. Pode nem sempre ser esse o caso. Caso contrário, 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 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
fields, 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 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
fields, 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 foram 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 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 fazer um loop pelos itens retornados e converter os bytes em um arquivo que pydicom
pode ser lido 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 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) ouAccept: multipart/related; type="application/octet-stream"; transfer-syntax=*
ouAccept: 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 a 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 os itens nunca existiram 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 do servidor (é o único elemento restante na série). 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 a comunicações digitais de informações médicas.