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:
- Accedere al portale di Azure.
- Cercare Risorse recenti e selezionare l'istanza del servizio DICOM.
- Copiare l'URL servizio del servizio DICOM.
- 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) oAccept: multipart/related; type="application/octet-stream"; transfer-syntax=*
oppureAccept: 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.