你当前正在访问 Microsoft Azure Global Edition 技术文档网站。 如果需要访问由世纪互联运营的 Microsoft Azure 中国技术文档网站,请访问 https://docs.azure.cn

将 DICOMweb 标准 API 与 Python 配合使用

本文介绍如何通过使用 Python 和示例 .dcm DICOM® 文件来使用 DICOMweb 服务。

使用以下示例文件:

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

示例 DICOM 文件的文件名、studyUID、seriesUID 和 instanceUID 为:

文件 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

注意

这些文件中的每一个都代表一个实例,并且是同一研究的一部分。 此外,绿色正方形和红色三角形属于同一系列,而蓝色圆形属于单独的系列。

先决条件

若要使用 DICOMweb 标准 API,必须部署 DICOM 服务实例。 有关详细信息,请参阅使用 Azure 门户部署 DICOM 服务

部署 DICOM 服务实例后,检索应用服务的 URL:

  1. 登录 Azure 门户
  2. 搜索“最近的资源”并选择你的 DICOM 服务实例
  3. 复制 DICOM 服务的服务 URL
  4. 如果没有令牌,请参阅使用 Azure CLI 获取用于 DICOM 服务的访问令牌

对于此代码,你需要访问公共预览版 Azure 服务。 请务必不要上传任何私人健康信息 (PHI)。

使用 DICOM 服务

DICOMweb 标准大量使用 multipart/related HTTP 请求(结合特定于 DICOM 的接受标头)。 熟悉其他基于 REST 的 API 的开发人员在使用 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 版本。 DICOM 服务的服务 URL 为:https://<workspacename-dicomservicename>.dicom.azurehealthcareapis.com。 可以使用 Azure 门户导航到 DICOM 服务并获取服务 URL。 请参阅 DICOM 服务的 API 版本控制文档,了解关于版本控制的更多信息。 如果使用自定义 URL,则需要用自己的 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 获取令牌以登录到服务。 还有其他凭据提供商可以使用,例如 ManagedIdentityCredentialEnvironmentCredential。 若要使用 AzureCliCredential,需要先从 CLI 登录到 Azure,然后再运行此代码。 有关详细信息,请参阅使用 Azure CLI 获取用于 DICOM 服务的访问令牌。 或者,复制并粘贴从 CLI 登录时检索的令牌。

注意

DefaultAzureCredential 返回多个不同的凭据对象。 我们将 AzureCliCredential 引用为返回的集合中的第 5 项。 情况并非总是如此。 如果不是,请取消评论 print(credential.credential) 行。 这会列出所有项。 查找正确的索引,重新调用 Python 使用从零开始的索引。

注意

如果尚未使用 CLI 登录到 Azure,此操作将失败。 必须从 CLI 登录到 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 库)不能以支持 DICOMweb 的方式使用 multipart\related。 由于这些库,我们必须添加一些方法来支持使用 DICOM 文件。

encode_multipart_related 采用一组字段(在 DICOM 的情况下,这些库通常是第 10 部分 dam 文件)和可选的用户定义的边界。 它会返回整个正文以及可以使用的 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 会话

创建一个名为 clientrequests 会话,用于与 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 中上传多个文件。 它有时用于在完整的系列或研究中上传多个实例。

详细信息

  • Path:../studies

  • 方法:POST

  • 标头:

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

    • Content-Type:上传的每个文件的 application/dicom,用边界值分隔

某些编程语言和工具的行为有所不同。 例如,有些语言和工具要求定义自己的边界。 对于这些语言和工具,可能需要使用稍作修改的 Content-Type 标头。 这些语言和工具可以成功使用。

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

存储特定研究的实例

此示例演示如何将多个 DICOM 文件上传到指定的研究中。 它使用 Python 将 DICOM 文件以字节形式预加载到内存中。

将文件数组传递给字段参数 encode_multipart_related 时,可以在单个 POST 中上传多个文件。 它有时用于上传完整的系列或研究。

详细信息

  • Path:../studies/{study}
  • 方法:POST
  • 标头:
    • Accept:application/dicom+json
    • Content-Type:multipart/related;type="application/dicom"
    • Authorization:Bearer $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 终结点,可将单个文件简化为请求正文中发送的二进制字节

详细信息

  • Path:../studies
  • 方法:POST
  • 标头:
    • Accept:application/dicom+json
    • Content-Type:application/dicom
    • Authorization:Bearer $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 实例。

检索研究中的所有实例

此示例检索单个研究中的所有实例。

详细信息

  • Path:../studies/{study}
  • 方法:GET
  • 标头:
    • Accept:multipart/related;type="application/dicom"; transfer-syntax=*
    • Authorization:Bearer $token"

之前上传的全部三个 dcm 文件都是同一研究的一部分,因此响应应返回全部三个实例。 验证响应的状态代码是否为 OK,以及是否返回了全部三个实例。

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)

检索研究中所有实例的元数据

此请求检索单个研究中所有实例的元数据。

详细信息

  • Path:../studies/{study}/metadata
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $token"

之前上传的全部三个 .dcm 文件都是同一研究的一部分,因此响应应返回全部三个实例的元数据。 验证响应的状态代码是否为 OK,以及是否返回了所有元数据。

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

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

检索系列中的所有实例

此请求检索单个系列中的所有实例。

详细信息

  • Path:../studies/{study}/series/{series}
  • 方法:GET
  • 标头:
    • Accept:multipart/related;type="application/dicom"; transfer-syntax=*
    • Authorization:Bearer $token"

此系列有两个实例(绿色正方形和红色三角形),因此响应应返回这两个实例。 验证响应的状态代码是否为 OK,以及是否返回了这两个实例。

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)

检索序列中所有实例的元数据

此请求检索单个序列中所有实例的元数据。

详细信息

  • Path:../studies/{study}/series/{series}/metadata
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $token"

此系列有两个实例(绿色正方形和红色三角形),因此响应应为这两个实例返回。 验证响应的状态代码是否为 OK,以及是否返回了这两个实例的元数据。

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)

检索一系列研究中的单个实例

此请求检索单个实例。

详细信息

  • Path:../studies/{study}/series{series}/instances/{instance}
  • 方法:GET
  • 标头:
    • Accept:application/dicom;transfer-syntax=*
    • Authorization:Bearer $token"

此代码示例应仅返回红色三角形实例。 验证响应的状态代码是否为 OK,以及是否返回了该实例。

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)

检索一系列研究中的单个实例的元数据

此请求检索单个研究和系列中单个实例的元数据。

详细信息

  • Path:../studies/{study}/series/{series}/instances/{instance}/metadata
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $token"

此代码示例应仅返回红色三角形实例的元数据。 验证响应的状态代码是否为 OK,以及是否返回了元数据。

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)

从单个实例中检索一个或多个帧

此请求从单个实例中检索一个或多个帧。

详细信息

  • Path:../studies/{study}/series{series}/instances/{instance}/frames/1,2,3
  • 方法:GET
  • 标头:
    • Authorization:Bearer $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";

此代码示例应返回红色三角形中的唯一帧。 验证响应的状态代码是否为 OK,以及是否返回了该帧。

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)

查询 DICOM (QIDO)

在以下示例中,我们使用项的唯一标识符来搜索项。 还可以搜索其他属性,例如 PatientName。

有关支持的 DICOM 属性,请参阅 DICOM 一致性声明

搜索检查

此请求按 DICOM 属性搜索一个或多个研究。

详细信息

  • Path:../studies?StudyInstanceUID={study}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 属性搜索一个或多个系列。

详细信息

  • Path:../series?SeriesInstanceUID={series}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 属性搜索单个研究中的一个或多个系列。

详细信息

  • Path:../studies/{study}/series?SeriesInstanceUID={series}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 属性搜索一个或多个实例。

详细信息

  • Path:../instances?SOPInstanceUID={instance}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 属性搜索单个研究中的一个或多个实例。

详细信息

  • Path:../studies/{study}/instances?SOPInstanceUID={instance}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 属性搜索单个研究和单个系列中的一个或多个实例。

详细信息

  • Path:../studies/{study}/series/{series}/instances?SOPInstanceUID={instance}
  • 方法:GET
  • 标头:
    • Accept:application/dicom+json
    • Authorization:Bearer $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 响应代码。

删除研究和系列中的特定实例

此请求删除单个研究和单个系列中单个实例。

详细信息

  • Path:../studies/{study}/series/{series}/instances/{instance}
  • 方法:DELETE
  • 标头:
    • Authorization:Bearer $token

此请求从服务器中删除红色三角形实例。 如果成功,则响应状态代码不包含任何内容。

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

删除研究中的特定系列

此请求删除单个研究中的单个系列(以及所有子实例)。

详细信息

  • Path:../studies/{study}/series/{series}
  • 方法:DELETE
  • 标头:
    • Authorization:Bearer $token

此代码示例从服务器中删除绿色正方形实例(它是系列中唯一留下的元素)。 如果成功,则响应状态代码不删除任何内容。

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

删除特定研究

此请求删除单个研究(以及所有子系列和实例)。

详细信息

  • Path:../studies/{study}
  • 方法:DELETE
  • 标头:
    • Authorization:Bearer $token
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers) 

注意

DICOM® 是美国电气制造商协会的注册商标,适用于其有关医疗信息数字通信的标准出版物。