你当前正在访问 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:
- 登录 Azure 门户。
- 搜索“最近的资源”并选择你的 DICOM 服务实例。
- 复制 DICOM 服务的服务 URL。
- 如果没有令牌,请参阅使用 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
获取令牌以登录到服务。 还有其他凭据提供商可以使用,例如 ManagedIdentityCredential
和 EnvironmentCredential
。 若要使用 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
会话
创建一个名为 client
的 requests
会话,用于与 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® 是美国电气制造商协会的注册商标,适用于其有关医疗信息数字通信的标准出版物。