Python で DICOMweb 標準 API を使用する
この記事では、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 |
Note
これらの各ファイルは 1 つのインスタンスを表し、同じ検査の一部です。 また、green-square と red-triangle は同じシリーズの一部であり、blue-circle は別のシリーズに含まれます。
前提条件
DICOMweb 標準 API を使用するには、DICOM サービスのインスタンスをデプロイする必要があります。 詳しくは、Azure portal を使用した DICOM サービスのデプロイに関する記事を参照してください。
DICOM サービスのインスタンスをデプロイしたら、アプリ サービスの URL を取得します。
- Azure portal にサインインします。
- [最近のリソース] を検索し、DICOM サービス インスタンスを選択します。
- DICOM サービスの [サービスの URL] をコピーします。
- トークンがない場合は、Azure CLI を使用して、DICOM サービスのアクセス トークンを取得する方法に関する記事を参照してください。
このコードでは、パブリック プレビューの Azure サービスにアクセスします。 個人医療情報 (PHI) をアップロードしないようにすることが重要です。
DICOM サービスを操作する
DICOMweb 標準は、multipart/related
HTTP 要求を DICOM 固有の Accept ヘッダーと組み合わせて頻繁に使用します。 他の REST ベースの API に慣れている開発者は、DICOMweb 標準を使いにくいと感じることがよくあります。 しかし、稼働し始めると、簡単に使用できます。 使用を開始するために、少し慣れる必要があるだけです。
Python ライブラリをインポートする
まず、必要な Python ライブラリをインポートします。
この例は、同期 requests
ライブラリを使用して実装します。 非同期サポートについては、httpx
または別の非同期ライブラリの使用を検討してください。 さらに、multipart/related
要求の操作をサポートするために、urllib3
から 2 つのサポート関数をインポートします。
さらに、Azure にログインしてトークンを取得するために、DefaultAzureCredential
をインポートします。
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 portal を使用して DICOM サービスに移動し、サービス URL を取得できます。 バージョン管理の詳細については、DICOM サービスの API バージョン管理に関するドキュメントも参照できます。 カスタム 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 からのサインイン時に取得したトークンをコピーして貼り付けます。
Note
DefaultAzureCredential
は、複数の異なる Credential オブジェクトを返します。 返されたコレクションの 5 番めの項目として AzureCliCredential
を参照します。 常にそうであるとは限りません。 そうでない場合は、print(credential.credential)
行のコメントを解除します。 これにより、すべての項目が一覧表示されます。 Python が 0 から始まるインデックス作成を使用していることを思い出して、正しいインデックスを見つけます。
Note
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 の場合、これらのライブラリは通常、Part 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
セッションを作成する
DICOM サービスとの通信に使用される client
と呼ばれる requests
セッションを作成します。
client = requests.session()
認証が正しく構成されていることを検証する
changefeed 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
を使用してインスタンスを保存する
この例は、1 つの DICOM ファイルをアップロードする方法を示します。また、ここでは、Python を使用して DICOM ファイルをバイト単位でメモリに事前に読み込んでいます。 ファイルの配列をフィールドのパラメーター encode_multipart_related
に渡すと、1 つの POST で複数のファイルをアップロードできます。 これは、完全なシリーズまたは検査内に複数のインスタンスをアップロードするためにときどき使用されます。
詳細:
パス: ../studies
メソッド: POST
Headers:
- 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
に渡すと、1 つの POST で複数のファイルをアップロードできます。 これは、完全なシリーズまたは検査をアップロードするためにときどき使用されます。
詳細:
- パス: ../studies/{study}
- メソッド: POST
- Headers:
- 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)
単一インスタンスを保存する (非標準)
次のコード例は、1 つの DICOM ファイルをアップロードする方法を示します。 これは、1 つのファイルのアップロードを要求の本文で送信されるバイナリ バイトに簡素化する非標準の API エンドポイントです
詳細:
- パス: ../studies
- メソッド: POST
- Headers:
- Accept: application/dicom+json
- Content-Type: application/dicom
- Authorization: Bearer $token"
- 本文:
- 1 つの 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 インスタンスの取得に焦点をあてています。
検査内のすべてのインスタンスを取得する
この例は、1 つの検査内のすべてのインスタンスを取得します。
詳細:
- パス: ../studies/{study}
- メソッド: GET
- Headers:
- Accept: multipart/related; type="application/dicom"; transfer-syntax=*
- Authorization: Bearer $token"
前にアップロードした 3 つの dcm ファイルはすべて同じ検査の一部であるため、応答は 3 つのインスタンスをすべて返すはずです。 応答の状態コードが OK で、3 つのインスタンスがすべて返されることを検証します。
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)
検査のすべてのインスタンスのメタデータを取得する
この要求は、1 つの検査内のすべてのインスタンスのメタデータを取得します。
詳細:
- パス: ../studies/{study}/metadata
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
前にアップロードした 3 つの .dcm
ファイルはすべて同じ検査の一部であるため、応答は 3 つのインスタンスすべてのメタデータを返すはずです。 応答の状態コードが 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)
シリーズ内のすべてのインスタンスを取得する
この要求は、1 つのシリーズ内のすべてのインスタンスを取得します。
詳細:
- パス: ../studies/{study}/series/{series}
- メソッド: GET
- Headers:
- Accept: multipart/related; type="application/dicom"; transfer-syntax=*
- Authorization: Bearer $token"
このシリーズには 2 つのインスタンス (緑の四角形と赤の三角形) があるため、応答は両方のインスタンスを返すはずです。 応答の状態コードが 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)
シリーズ内のすべてのインスタンスのメタデータを取得する
この要求は、1 つのシリーズ内のすべてのインスタンスのメタデータを取得します。
詳細:
- パス: ../studies/{study}/series/{series}/metadata
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
このシリーズには 2 つのインスタンス (緑の四角形と赤の三角形) があるため、応答は両方のインスタンスに対して返されるはずです。 応答の状態コードが 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)
検査のシリーズ内の 1 つのインスタンスを取得する
この要求は 1 つのインスタンスを取得します。
詳細:
- パス: ../studies/{study}/series{series}/instances/{instance}
- メソッド: GET
- Headers:
- 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)
検査のシリーズ内の 1 つのインスタンスのメタデータを取得する
この要求は、1 つの検査とシリーズに含まれる 1 つのインスタンスのメタデータを取得します。
詳細:
- パス: ../studies/{study}/series/{series}/instances/{instance}/metadata
- メソッド: GET
- Headers:
- 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)
1 つのインスタンスから 1 つ以上のフレームを取得する
この要求は、1 つのインスタンスから 1 つ以上のフレームを取得します。
詳細:
- パス: ../studies/{study}/series{series}/instances/{instance}/frames/1,2,3
- メソッド: GET
- Headers:
- 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 属性によって 1 つ以上の検査を検索します。
詳細:
- パス: ../studies?StudyInstanceUID={study}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つの検査が含まれていて、応答コードが 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)
シリーズを検索する
この要求は、DICOM 属性によって 1 つ以上のシリーズを検索します。
詳細:
- パス: ../series?SeriesInstanceUID={series}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つのシリーズが含まれていて、応答コードが 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)
1 つの検査内のシリーズを検索する
この要求は、DICOM 属性によって 1 つの検査内の 1 つ以上のシリーズを検索します。
詳細:
- パス: ../studies/{study}/series?SeriesInstanceUID={series}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つのシリーズが含まれていて、応答コードが 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)
インスタンスを検索する
この要求は、DICOM 属性によって 1 つ以上のインスタンスを検索します。
詳細:
- パス: ../instances?SOPInstanceUID={instance}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つのインスタンスが含まれていて、応答コードが 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)
1 つの検査内のインスタンスを検索する
この要求は、DICOM 属性によって 1 つの検査内の 1 つ以上のインスタンスを検索します。
詳細:
- パス: ../studies/{study}/instances?SOPInstanceUID={instance}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つのインスタンスが含まれていて、応答コードが 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)
1 つの検査とシリーズ内のインスタンスを検索する
この要求は、DICOM 属性によって 1 つの検査および 1 つのシリーズ内の 1 つ以上のインスタンスを検索します。
詳細:
- パス: ../studies/{study}/series/{series}/instances?SOPInstanceUID={instance}
- メソッド: GET
- Headers:
- Accept: application/dicom+json
- Authorization: Bearer $token"
応答に 1 つのインスタンスが含まれていて、応答コードが 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)
DICOM を削除する
Note
削除は DICOM 標準の一部ではありませんが、便宜上追加されています。
削除が成功すると、204 応答コードが返されます。 項目が存在しないか、既に削除されている場合は、404 応答コードが返されます。
1 つの検査およびシリーズ内の特定のインスタンスを削除する
この要求は、1 つの検査および 1 つのシリーズ内の 1 つのインスタンスを削除します。
詳細:
- パス: ../studies/{study}/series/{series}/instances/{instance}
- メソッド: DELETE
- Headers:
- 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)
1 つの検査内の特定のシリーズを削除する
この要求は、1 つの検査内の 1 つのシリーズ (およびすべての子インスタンス) を削除します。
詳細:
- パス: ../studies/{study}/series/{series}
- メソッド: DELETE
- Headers:
- Authorization: Bearer $token
このコード例は、サーバーから緑の四角形のインスタンス (シリーズ内に残っている唯一の要素) を削除します。 成功した場合、応答状態コードはコンテンツを削除しません。
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}/series/{series_uid}'
response = client.delete(url, headers=headers)
特定の検査を削除する
この要求は、1 つの検査 (およびすべての子シリーズとインスタンス) を削除します。
詳細:
- パス: ../studies/{study}
- メソッド: DELETE
- Headers:
- Authorization: Bearer $token
headers = {"Authorization":bearer_token}
url = f'{base_url}/studies/{study_uid}'
response = client.delete(url, headers=headers)
Note
DICOM® は、医療情報のデジタル通信に関する標準出版物に関する米国電機工業会 (National Electrical Manufacturers Association) の登録商標です。