Использование API DICOMweb Standard с Python

В этой статье показано, как работать со службой DICOMweb с помощью Python и примеров .dcm ФАЙЛОВ DICOM®.

Используйте следующие примеры файлов:

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

Имя файла, studyUID, seriesUID и instanceUID примера файлов DICOM:

Файлы 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

Примечание.

Каждый из этих файлов представляет один экземпляр и является частью одного исследования. Кроме того, зеленый квадрат и красный треугольник являются частью одной серии, в то время как синий круг находится в отдельной серии.

Необходимые компоненты

Чтобы использовать API DICOMweb Standard, необходимо развернуть экземпляр службы DICOM. Дополнительные сведения см. в статье "Развертывание службы DICOM с помощью портал Azure".

После развертывания экземпляра службы DICOM получите URL-адрес для службы приложений:

  1. Войдите на портал Azure.
  2. Выполните поиск последних ресурсов и выберите экземпляр службы DICOM.
  3. Скопируйте URL-адрес службы DICOM.
  4. Если у вас нет маркера, ознакомьтесь с маркером доступа для службы DICOM с помощью Azure CLI.

Для этого кода вы можете получить доступ к службе Общедоступной предварительной версии Azure. Важно, чтобы вы не загружали какие-либо частные сведения о работоспособности (PHI).

Работа со службой DICOM

DiCOMweb Standard использует http-запросы в сочетании multipart/related с определенными заголовками приема DICOM. Разработчики, знакомые с другими API на основе REST, часто находят работу со стандартом DICOMweb. Тем не менее, после того, как он работает и работает, это легко использовать. Это просто занимает немного знакомства, чтобы приступить к работе.

Импорт библиотек Python

Сначала импортируйте необходимые библиотеки Python.

Мы реализуем этот пример с помощью синхронной requests библиотеки. Для асинхронной поддержки рекомендуется использовать httpx или другую асинхронную библиотеку. Кроме того, мы импортируем две вспомогательные функции из urllib3 поддержки работы с multipart/related запросами.

Кроме того, мы импортируем DefaultAzureCredential вход в Azure и получаем маркер.

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

Настройка определяемых пользователем переменных

Замените все значения переменных, упакованные в { } собственными значениями. Кроме того, убедитесь, что все созданные переменные верны. Например, base_url создается с помощью URL-адреса службы, а затем добавляется версия используемого REST API. URL-адрес службы DICOM: https://<workspacename-dicomservicename>.dicom.azurehealthcareapis.com Вы можете использовать портал Azure для перехода к службе DICOM и получения URL-адреса службы. Вы также можете ознакомиться с документацией по версиям API для службы DICOM, чтобы получить дополнительные сведения о управлении версиями. Если вы используете пользовательский URL-адрес, необходимо переопределить это значение собственным.

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

Проверка подлинности в Azure и получение маркера

DefaultAzureCredential позволяет использовать различные способы получения маркеров для входа в службу. В этом примере используйте AzureCliCredential маркер для входа в службу. Существуют другие поставщики учетных данных, такие как ManagedIdentityCredential и EnvironmentCredential которые вы можете использовать. Чтобы использовать AzureCliCredential, необходимо войти в Azure из ИНТЕРФЕЙСА командной строки перед выполнением этого кода. Дополнительные сведения см. в статье "Получение маркера доступа для службы DICOM" с помощью Azure CLI. Кроме того, скопируйте и вставьте маркер, полученный при входе из ИНТЕРФЕЙСА командной строки.

Примечание.

DefaultAzureCredential возвращает несколько различных объектов Credential. Мы ссылаемся на AzureCliCredential этот элемент как 5-й элемент в возвращаемой коллекции. Это может быть не всегда так. Если нет, раскомментируйте print(credential.credential) строку. В этом списке будут перечислены все элементы. Найдите правильный индекс, напомнив, что Python использует индексирование на основе нуля.

Примечание.

Если вы не вошли в Azure с помощью ИНТЕРФЕЙСА командной строки, это завершится ошибкой. Для работы в Azure необходимо войти в Azure из ИНТЕРФЕЙСА командной строки.

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

Создание вспомогательных методов для поддержки multipart\related

Библиотеки Requests (и большинство библиотек Python) не работают так multipart\related , как поддерживает DICOMweb. Из-за этих библиотек необходимо добавить несколько методов для поддержки работы с файлами DICOM.

encode_multipart_related принимает набор полей (в случае DICOM эти библиотеки обычно являются частью 10 файлов плотины) и необязательной пользовательской границы. Он возвращает как полный текст, так и content_type, который можно использовать.

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

Создание сеанса requests

requests Создает сеанс, который client используется для взаимодействия со службой DICOM.

client = requests.session()

Проверка правильной настройки проверки подлинности

Вызовите конечную точку API измененного канала, которая возвращает 200, если проверка подлинности выполнена успешно.

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

Отправка экземпляров DICOM (STOW)

В следующих примерах выделено сохранение файлов DICOM.

Хранение экземпляров с помощью multipart/related

В этом примере показано, как отправить один ФАЙЛ DICOM, и он использует Python для предварительной загрузки ФАЙЛА DICOM в память как байты. При передаче массива файлов в параметр encode_multipart_relatedполей можно отправить несколько файлов в одном post. Иногда он используется для отправки нескольких экземпляров внутри полной серии или изучения.

Сведения:

  • Путь:.. /учёба

  • Метод: POST

  • Заголовки:

    • Принять: application/dicom+json
    • Тип контента: многопартийное или связанное; type="application/dicom"
    • Авторизация: носитель $token"
  • Текст:

    • Content-Type: application/dicom для каждого отправленного файла, разделенного значением границы

Некоторые языки программирования и средства ведут себя по-разному. Например, некоторые требуют, чтобы определить собственную границу. Для этих языков и инструментов может потребоваться использовать немного измененный заголовок Content-Type. Эти языки и средства можно использовать успешно.

  • Тип контента: многопартийное или связанное; type="application/dicom"; boundary=ABCD1234
  • Тип контента: многопартийное или связанное; boundary=ABCD1234
  • Тип контента: многопартийное или связанное
#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)

Хранение экземпляров для конкретного исследования

В этом примере показано, как передать несколько DICOM-файлов в указанное исследование. Он использует Python для предварительной загрузки ФАЙЛА DICOM в память в виде байтов.

При передаче массива файлов в параметр encode_multipart_relatedполей можно отправить несколько файлов в одном post. Иногда он используется для отправки полной серии или изучения.

Сведения:

  • Путь:.. /studies/{study}
  • Метод: POST
  • Заголовки:
    • Принять: application/dicom+json
    • Тип контента: многопартийное или связанное; type="application/dicom"
    • Авторизация: носитель $token"
  • Текст:
    • Content-Type: application/dicom для каждого отправленного файла, разделенного значением границы

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)

Хранение одного экземпляра (нестандартное)

В следующем примере кода показано, как отправить один файл DICOM. Это нестандартная конечная точка API, которая упрощает отправку одного файла в виде двоичных байтов, отправленных в тексте запроса

Сведения:

  • Путь:.. /учёба
  • Метод: POST
  • Заголовки:
    • Принять: application/dicom+json
    • Content-Type: application/dicom
    • Авторизация: носитель $token"
  • Текст:
    • Содержит один файл DICOM в виде двоичных байтов.
#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

Получение экземпляров DICOM (WADO)

В следующих примерах выделено получение экземпляров DICOM.

Получение всех экземпляров в исследовании

В этом примере извлекаются все экземпляры в одном исследовании.

Сведения:

  • Путь:.. /studies/{study}
  • Метод: GET
  • Заголовки:
    • Принять: многопартийное или связанное; type="application/dicom"; transfer-syntax=*
    • Авторизация: носитель $token"

Все три отправленных ранее файла dcm являются частью одного исследования, поэтому ответ должен возвращать все три экземпляра. Убедитесь, что ответ имеет код состояния "ОК" и возвращается ли все три экземпляра.

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)

Использование извлеченных экземпляров

Экземпляры извлекаются как двоичные байты. Вы можете прокрутить возвращаемые элементы и преобразовать байты в файл, который pydicom можно прочитать следующим образом.

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)

Получение метаданных всех экземпляров в исследовании

Этот запрос извлекает метаданные для всех экземпляров в одном исследовании.

Сведения:

  • Путь:.. /studies/{study}/метаданные
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Все три .dcm отправленных ранее файла являются частью одного исследования, поэтому ответ должен возвращать метаданные для всех трех экземпляров. Убедитесь, что ответ имеет код состояния ОК и возвращается все метаданные.

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

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

Получение всех экземпляров в серии

Этот запрос извлекает все экземпляры в одной серии.

Сведения:

  • Путь:.. /studies/{study}/series/{series}
  • Метод: GET
  • Заголовки:
    • Принять: многопартийное или связанное; type="application/dicom"; transfer-syntax=*
    • Авторизация: носитель $token"

В этой серии есть два экземпляра (зеленый квадрат и красный треугольник), поэтому ответ должен возвращать оба экземпляра. Убедитесь, что ответ имеет код состояния "ОК", а оба экземпляра возвращаются.

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)

Получение метаданных всех экземпляров в рядах

Этот запрос извлекает метаданные для всех экземпляров в одной серии.

Сведения:

  • Путь:.. /studies/{study}/series/{series}/metadata
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

В этой серии есть два экземпляра (зеленый квадрат и красный треугольник), поэтому ответ должен возвращаться для обоих экземпляров. Убедитесь, что ответ имеет код состояния "ОК" и возвращается метаданные обоих экземпляров.

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)

Получение одного экземпляра в серии исследования

Этот запрос извлекает один экземпляр.

Сведения:

  • Путь:.. /studies/{study}/series{series}/instances/{instance}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom; transfer-syntax=*
    • Авторизация: носитель $token"

Этот пример кода должен возвращать только красный треугольник экземпляра. Убедитесь, что ответ имеет код состояния "ОК" и возвращается ли экземпляр.

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)

Получение метаданных одного экземпляра в серии исследований

Этот запрос извлекает метаданные для одного экземпляра в рамках одного исследования и ряда.

Сведения:

  • Путь:.. /studies/{study}/series/{series}/instances/{instance}/metadata
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Этот пример кода должен возвращать только метаданные для красного треугольника экземпляра. Убедитесь, что ответ имеет код состояния "ОК" и возвращается ли метаданные.

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)

Получение одного или нескольких кадров из одного экземпляра

Этот запрос извлекает один или несколько кадров из одного экземпляра.

Сведения:

  • Путь:.. /studies/{study}/series{series}/instances/{instance}/frames/1,2,3
  • Метод: GET
  • Заголовки:
    • Авторизация: носитель $token"
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=1.2.840.10008.1.2.1 (по умолчанию) или
    • Accept: multipart/related; type="application/octet-stream"; transfer-syntax=* или
    • Accept: multipart/related; type="application/octet-stream";

Этот пример кода должен возвращать единственный кадр из красного треугольника. Убедитесь, что ответ имеет код состояния "ОК" и возвращается кадр.

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)

В следующих примерах мы ищем элементы, используя их уникальные идентификаторы. Вы также можете искать другие атрибуты, например PatientName.

См. инструкцию соответствия DICOM для поддерживаемых атрибутов DICOM.

Поиск исследований

Этот запрос выполняет поиск одного или нескольких исследований по атрибутам DICOM.

Сведения:

  • Путь:.. /учёба? StudyInstanceUID={study}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит одно исследование и что код ответа ОК.

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)

Поиск рядов

Этот запрос выполняет поиск одной или нескольких рядов по атрибутам DICOM.

Сведения:

  • Путь:.. /серия? SeriesInstanceUID={series}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит одну серию и что код ответа ОК.

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)

Поиск ряда в исследовании

Этот запрос выполняет поиск одной или нескольких рядов в рамках одного исследования атрибутами DICOM.

Сведения:

  • Путь:.. /studies/{study}/series? SeriesInstanceUID={series}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит одну серию и что код ответа ОК.

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)

Поиск экземпляров

Этот запрос выполняет поиск одного или нескольких экземпляров по атрибутам DICOM.

Сведения:

  • Путь:.. /Экземпляров? SOPInstanceUID={instance}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит один экземпляр и что код ответа ОК.

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)

Поиск экземпляров в исследовании

Этот запрос выполняет поиск одного или нескольких экземпляров в рамках одного исследования атрибутами DICOM.

Сведения:

  • Путь:.. /studies/{study}/instances? SOPInstanceUID={instance}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит один экземпляр и что код ответа ОК.

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)

Поиск экземпляров в рамках исследования и ряда

Этот запрос выполняет поиск одного или нескольких экземпляров в рамках одного исследования и одного ряда по атрибутам DICOM.

Сведения:

  • Путь:.. /studies/{study}/series/{series}/instances? SOPInstanceUID={instance}
  • Метод: GET
  • Заголовки:
    • Принять: application/dicom+json
    • Авторизация: носитель $token"

Убедитесь, что ответ содержит один экземпляр и что код ответа ОК.

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)

Удаление DICOM

Примечание.

Удаление не является частью стандарта DICOM, но оно было добавлено для удобства.

Код ответа 204 возвращается при успешном удалении. Код ответа 404 возвращается, если элементы никогда не существовали или уже удалены.

Удаление конкретного экземпляра в рамках исследования и ряда

Этот запрос удаляет один экземпляр в рамках одного исследования и одной серии.

Сведения:

  • Путь:.. /studies/{study}/series/{series}/instances/{instance}
  • Метод: DELETE
  • Заголовки:
    • Авторизация: $token носителя

Этот запрос удаляет экземпляр красного треугольника с сервера. В случае успешного выполнения код состояния ответа не содержит содержимого.

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

Удаление определенной серии в исследовании

Этот запрос удаляет один ряд (и все дочерние экземпляры) в рамках одного исследования.

Сведения:

  • Путь:.. /studies/{study}/series/{series}
  • Метод: DELETE
  • Заголовки:
    • Авторизация: $token носителя

Этот пример кода удаляет экземпляр зеленого квадрата с сервера (это единственный элемент, оставшийся в серии). В случае успешного выполнения код состояния ответа не удаляет содержимое.

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

Удаление конкретного исследования

Этот запрос удаляет одно исследование (и все дочерние ряды и экземпляры).

Сведения:

  • Путь:.. /studies/{study}
  • Метод: DELETE
  • Заголовки:
    • Авторизация: $token носителя
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

Примечание.

DICOM® является зарегистрированным товарным знаком Национальной ассоциации производителей электрических технологий для публикаций по стандартам, касающихся цифровых коммуникаций медицинской информации.