Condividi tramite


Usare le API DICOMweb standard con Python

Questo articolo illustra come usare il servizio DICOMweb con Python e file DICOM® con estensione dcm di esempio.

Usare questi file di esempio:

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

Il nome file e i valori studyUID, seriesUID e instanceUID dei file DICOM di esempio sono:

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

Nota

Ognuno di questi file rappresenta una singola istanza e fa parte dello stesso studio. I file green-square e red-triangle fanno inoltre parte della stessa serie, mentre il file blue-circle si trova in una serie separata.

Prerequisiti

Per usare le API DICOMweb Standard, è necessario avere un'istanza del servizio DICOM distribuita. Per altre informazioni, vedere Distribuire il servizio DICOM usando il portale di Azure.

Dopo aver distribuito un'istanza del servizio DICOM, recuperare l'URL per il servizio app:

  1. Accedere al portale di Azure.
  2. Cercare Risorse recenti e selezionare l'istanza del servizio DICOM.
  3. Copiare l'URL servizio del servizio DICOM.
  4. Se non si ha un token, vedere Ottenere il token di accesso per il servizio DICOM usando l'interfaccia della riga di comando di Azure.

Per questo codice si accede a un servizio di Azure disponibile in anteprima pubblica. È importante non caricare informazioni sanitarie protette.

Usare il servizio DICOM

DICOMweb Standard usa un numero elevato di richieste HTTP multipart/related combinate con intestazioni di accettazione specifiche di DICOM. Gli sviluppatori che hanno familiarità con altre API basate su REST spesso riscontrano problemi con lo standard DICOMweb. Quando è operativo, risulta tuttavia facile da usare. Occorre acquisire un po' di familiarità per iniziare.

Importare le librerie Python

Prima di tutto, importare le librerie Python necessarie.

Questo esempio viene implementato usando la libreria requests sincrona. Per il supporto asincrono è consigliabile usare httpx o un'altra libreria asincrona. Vengono inoltre importate due funzioni di supporto da urllib3 per supportare l'uso delle richieste multipart/related.

Viene inoltre eseguita l'importazione anche di DefaultAzureCredential per accedere ad Azure e ottenere un token.

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

Configurare le variabili definite dall'utente

Sostituire tutti i valori delle variabili di cui è stato eseguito il wrapping in { } con i propri valori. Verificare inoltre che le variabili costruite siano corrette. Ad esempio, la variabile base_url viene costruita usando l'URL del servizio, aggiunto alla versione dell'API REST in uso. L'URL del servizio DICOM è: https://<workspacename-dicomservicename>.dicom.azurehealthcareapis.com. È possibile usare il portale di Azure per passare al servizio DICOM e ottenere l'URL del servizio. Per altre informazioni sul controllo delle versioni, vedere anche la documentazione sul controllo delle versioni dell'API per il servizio DICOM. Se si usa un URL personalizzato, è necessario eseguire l'override di tale valore con il proprio.

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

Eseguire l'autenticazione in Azure e ottenere un token

DefaultAzureCredential consente di usare vari modi per ottenere i token per accedere al servizio. In questo esempio usare AzureCliCredential per ottenere un token per accedere al servizio. Esistono altri provider di credenziali, ad esempio ManagedIdentityCredential e EnvironmentCredential, che è possibile usare. Per usare AzureCliCredential, è necessario accedere ad Azure dall'interfaccia della riga di comando prima di eseguire questo codice. Per altre informazioni, vedere Ottenere il token di accesso per il servizio DICOM usando l'interfaccia della riga di comando di Azure. In alternativa, copiare e incollare il token recuperato durante l'accesso dall'interfaccia della riga di comando.

Nota

DefaultAzureCredential restituisce diversi oggetti Credential. Si fa riferimento a AzureCliCredential come 5° elemento nella raccolta restituita. È possibile che lo scenario effettivo sia diverso. In tale caso, rimuovere il commento dalla riga print(credential.credential). Verranno elencati tutti gli elementi. Trovare l'indice corretto, ricordando che Python usa l'indicizzazione in base zero.

Nota

Se non è stato eseguito l'accesso ad Azure tramite l'interfaccia della riga di comando, l'operazione avrà esito negativo. Per un funzionamento corretto, è necessario accedere ad Azure dall'interfaccia della riga di comando.

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}'

Creare metodi di supporto per supportare multipart\related

Le librerie Requests (e la maggior parte delle librerie Python) non funzionano con multipart\related in un modo che consente di supportare DICOMweb. A causa di queste librerie, è necessario aggiungere alcuni metodi per supportare l'uso dei file DICOM.

encode_multipart_related accetta un set di campi (nel caso DICOM, queste librerie sono in genere file DAM Part 10) e un limite facoltativo definito dall'utente. Restituisce il corpo completo e il valore content_type, che può essere utilizzato.

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

Crea una sessione requests.

Crea una sessione requests, denominata client, utilizzata per comunicare con il servizio DICOM.

client = requests.session()

Verificare che l'autenticazione sia configurata correttamente

Chiamare l'endpoint API changefeed, che restituisce un valore pari a 200 se l'autenticazione ha esito positivo.

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

Caricare istanze DICOM (STOW)

Negli esempi seguenti viene evidenziato il salvataggio permanente dei file DICOM.

Archiviare le istanze usando multipart/related

Questo esempio illustra come caricare un singolo file DICOM e usa Python per precaricare il file DICOM in memoria come byte. Quando una matrice di file viene passata al parametro fields encode_multipart_related, è possibile caricare più file in un singolo POST. A volte viene usato per caricare diverse istanze all'interno di una serie completa o di uno studio.

Dettagli:

  • Percorso: ../studies

  • Metodo: POST

  • Intestazioni:

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

    • Content-Type: application/dicom per ogni file caricato, separato da un valore limite

Alcuni linguaggi e strumenti di programmazione si comportano in modo diverso. Ad esempio, alcuni richiedono di definire un limite personalizzato. Per questi linguaggi e strumenti potrebbe essere necessario usare un'intestazione Content-Type leggermente modificata. Questi linguaggi e strumenti possono essere usati con esito positivo.

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

Archiviare istanze per uno studio specifico

Questo esempio illustra come caricare più file DICOM nello studio specificato. Usa Python per precaricare il file DICOM in memoria come byte.

Quando una matrice di file viene passata al parametro fields encode_multipart_related, è possibile caricare più file in un singolo POST. A volte viene usato per caricare una serie completa o uno studio.

Dettagli:

  • Percorso: ../studies/{studio}
  • Metodo: POST
  • Intestazioni:
    • Accept: application/dicom+json
    • Content-Type: multipart/related; type="application/dicom"
    • Authorization: Bearer $token"
  • Corpo:
    • Content-Type: application/dicom per ogni file caricato, separato da un valore 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)

Archiviare un'istanza singola (non standard)

Nell'esempio di codice seguente viene illustrato come caricare un singolo file DICOM. Si tratta di un endpoint API non standard che semplifica il caricamento di un singolo file come byte binari inviati nel corpo di una richiesta

Dettagli:

  • Percorso: ../studies
  • Metodo: POST
  • Intestazioni:
    • Accept: application/dicom+json
    • Content-Type: application/dicom
    • Authorization: Bearer $token"
  • Corpo:
    • Contiene un singolo file DICOM come byte binari.
#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

Recuperare istanze di DICOM (WADO)

Negli esempi seguenti viene illustrato il recupero delle istanze di DICOM.

Recuperare tutte le istanze all'interno di uno studio

In questo esempio vengono recuperate tutte le istanze all'interno di un singolo studio.

Dettagli:

  • Percorso: ../studies/{studio}
  • Metodo: GET
  • Intestazioni:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Tutti e tre i file DCM caricati in precedenza fanno parte dello stesso studio, quindi la risposta dovrebbe restituire tutte e tre le istanze. Verificare che la risposta abbia un codice di stato OK e che vengano restituite tutte e tre le istanze.

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)

Usare le istanze recuperate

Le istanze vengono recuperate come byte binari. È possibile scorrere in ciclo gli elementi restituiti e convertire i byte in un file che pydicom può leggere come segue.

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)

Recuperare i metadati di tutte le istanze in uno studio

Questa richiesta recupera i metadati per tutte le istanze all'interno di un singolo studio.

Dettagli:

  • Percorso: ../studies/{studio}/metadata
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Tutti e tre i file .dcm caricati in precedenza fanno parte dello stesso studio, quindi la risposta dovrebbe restituire i metadati per tutte e tre le istanze. Verificare che la risposta abbia un codice di stato OK e che vengano restituiti tutti i metadati.

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

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

Recuperare tutte le istanze all'interno di una serie

Questa richiesta recupera tutte le istanze all'interno di una singola serie.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}
  • Metodo: GET
  • Intestazioni:
    • Accept: multipart/related; type="application/dicom"; transfer-syntax=*
    • Authorization: Bearer $token"

Questa serie ha due istanze (green-square e red-triangle), quindi la risposta deve restituire entrambe le istanze. Verificare che la risposta abbia un codice di stato OK e che vengano restituite entrambe le istanze.

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)

Recuperare i metadati di tutte le istanze in una serie

Questa richiesta recupera i metadati per tutte le istanze all'interno di una singola serie.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}/metadata
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Questa serie ha due istanze (green-square e red-triangle), quindi la risposta deve restituire valori per entrambe le istanze. Verificare che la risposta abbia un codice di stato OK e che vengano restituiti i metadati di entrambe le istanze.

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)

Recuperare una singola istanza all'interno di una serie di uno studio

Questa richiesta recupera una singola istanza.

Dettagli:

  • Percorso: ../studies/{studio}/series{serie}/instances/{istanza}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom; transfer-syntax=*
    • Authorization: Bearer $token"

Questo esempio di codice deve restituire solo il file red-triangle dell'istanza. Verificare che la risposta abbia un codice di stato OK e che venga restituita l'istanza.

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)

Recuperare i metadati di una singola istanza all'interno di una serie di uno studio

Questa richiesta recupera i metadati per una singola istanza all'interno di un singolo studio e serie.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}/instances/{istanza}/metadata
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Questo esempio di codice deve restituire solo i metadati per il file red-triangle dell'istanza. Verificare che la risposta abbia un codice di stato OK e che vengano restituiti i metadati.

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)

Recuperare uno o più frame da una singola istanza

Questa richiesta recupera uno o più frame da una singola istanza.

Dettagli:

  • Percorso: ../studies/{studio}/series{serie}/instances/{istanza}/frames/1,2,3
  • Metodo: GET
  • Intestazioni:
    • Authorization: Bearer $token"
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1 (valore predefinito) o
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=* oppure
    • Accept: multipart/related; type="application/octet-stream";

Questo esempio di codice deve restituire l'unico frame dal file red-triangle. Verificare che la risposta abbia un codice di stato OK e che venga restituito il frame.

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)

Negli esempi seguenti viene eseguita la ricerca di elementi usando i rispettivi identificatori univoci. È anche possibile cercare altri attributi, ad esempio PatientName.

Per informazioni sugli attributi DICOM supportati, vedere la Dichiarazione di conformità DICOM.

Ricerca di studi

Questa richiesta cerca uno o più studi in base agli attributi DICOM.

Dettagli:

  • Percorso: ../studies?StudyInstanceUID={studio}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa uno studio e che il codice di risposta sia 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)

Cercare serie

Questa richiesta cerca uno o più serie in base agli attributi DICOM.

Dettagli:

  • Percorso: ../series?SeriesInstanceUID={serie}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa una serie e che il codice di risposta sia 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)

Cercare serie all'interno di uno studio

Questa richiesta cerca una o più serie all'interno di un singolo studio in base agli attributi DICOM.

Dettagli:

  • Percorso: ../studies/{studio}/series?SeriesInstanceUID={serie}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa una serie e che il codice di risposta sia 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)

Cercare istanze

Questa richiesta cerca uno o più istanze in base agli attributi DICOM.

Dettagli:

  • Percorso: ../instances?SOPInstanceUID={istanza}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa un'istanza e che il codice di risposta sia 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)

Cercare istanze all'interno di uno studio

Questa richiesta cerca una o più istanze all'interno di un singolo studio in base agli attributi DICOM.

Dettagli:

  • Percorso: ../studies/{studio}/instances?SOPInstanceUID={istanza}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa un'istanza e che il codice di risposta sia 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)

Cercare istanze all'interno di uno studio e una serie

Questa richiesta cerca una o più istanze all'interno di un singolo studio e una singola serie in base agli attributi DICOM.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}/instances?SOPInstanceUID={istanza}
  • Metodo: GET
  • Intestazioni:
    • Accept: application/dicom+json
    • Authorization: Bearer $token"

Verificare che la risposta includa un'istanza e che il codice di risposta sia 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)

Eliminare DICOM

Nota

L'eliminazione non fa parte dello standard DICOM, ma è stata aggiunta per praticità.

Quando l'eliminazione ha esito positivo, viene restituito un codice di risposta 204. Se gli elementi non sono mai esistiti o sono già stati eliminati, viene restituito un codice di risposta 404.

Eliminare un'istanza specifica all'interno di uno studio e di una serie

Questa richiesta elimina una singola istanza all'interno di un singolo studio e di una singola serie.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}/instances/{istanza}
  • Metodo: DELETE
  • Intestazioni:
    • Authorization: Bearer $token

Questa richiesta elimina l'istanza del triangolo rosso dal server. In caso di esito positivo, il codice di stato della risposta non include contenuto.

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

Eliminare una serie specifica all'interno di uno studio

Questa richiesta elimina una singola serie (e tutte le istanze figlio) all'interno di un singolo studio.

Dettagli:

  • Percorso: ../studies/{studio}/series/{serie}
  • Metodo: DELETE
  • Intestazioni:
    • Authorization: Bearer $token

Questo esempio di codice elimina l'istanza del file green-square dal server. Si tratta dell'unico elemento rimasto nella serie. In caso di esito positivo, il codice di stato della risposta non elimina il contenuto.

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

Eliminare uno studio specifico

Questa richiesta elimina un singolo studio (e tutte le serie e le istanze figlio).

Dettagli:

  • Percorso: ../studies/{studio}
  • Metodo: DELETE
  • Intestazioni:
    • Authorization: Bearer $token
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

Nota

DICOM® è il marchio registrato della National Electrical Manufacturers Association per le sue pubblicazioni Standard relative alle comunicazioni digitali delle informazioni mediche.