Skip to content

DICOMweb

pydcm.dicomweb is a QIDO / WADO / STOW / DELETE client over the native HTTP stack — query, retrieve (streaming), store and delete against a remote server.

Connecting: server + base_path

Address a DICOMweb server by an origin (scheme://host:port) plus a base path — the prefix the server mounts its DICOMweb endpoints under. The base path is different on every product, and getting it wrong is the #1 cause of "it returns nothing / a 404 / odd IDs":

Server server (origin) base_path
Orthanc http://host:8042 /dicom-web
dcm4chee-arc http://host:8080 /dcm4chee-arc/aets/DCM4CHEE/rs
root-mounted server http://host:8080 "" (empty)
from pydcm import dicomweb

# Orthanc
dicomweb.search_studies("http://localhost:8042", base_path="/dicom-web")

# dcm4chee-arc (the AE in the path is the archive AE Title, default DCM4CHEE)
dicomweb.search_studies("http://localhost:8080",
                        base_path="/dcm4chee-arc/aets/DCM4CHEE/rs")

# a root-mounted server: base_path is the empty string (NOT "/", which 404s)
dicomweb.search_studies("http://localhost:8080", base_path="")

A prefix may instead be folded into server ("http://localhost:8042/dicom-web") — both spell the same endpoint — but prefer the explicit base_path. base_path needs a leading slash ("/dicom-web", not "dicom-web").

Auth is the full Authorization header value, passed as auth=:

dicomweb.search_studies("http://localhost:8042", base_path="/dicom-web",
                        auth="Basic b3J0aGFuYzpvcnRoYW5j")     # Orthanc default orthanc:orthanc
dicomweb.search_studies("http://localhost:8080", base_path="/dcm4chee-arc/aets/DCM4CHEE/rs",
                        auth="Bearer eyJ…")                    # dcm4chee behind Keycloak

HTTPS works by passing an https:// origin; trust a private/self-signed CA via the SSL_CERT_FILE environment variable (there is no per-call CA argument).

The rest of this page omits base_path=/auth= for brevity — add them per the table above.

Query (QIDO-RS) → DICOM-JSON

studies   = dicomweb.search_studies("http://localhost:8042", base_path="/dicom-web",
                                    matches={"PatientID": "42"})
series    = dicomweb.search_series("http://localhost:8042", study_uid, base_path="/dicom-web")
instances = dicomweb.search_instances("http://localhost:8042", study_uid, series_uid,
                                      base_path="/dicom-web")

Each returns a list[dict] of DICOM-JSON ({} on 204 No Content). Pass includefields=[...] to widen the returned attributes, limit=/offset= to page.

Retrieve (WADO-RS)

# whole objects → list[bytes] of Part-10
parts = dicomweb.retrieve_study("http://localhost:8042", study_uid, base_path="/dicom-web")
inst  = dicomweb.retrieve_instance("http://localhost:8042", study_uid, series_uid, sop_uid,
                                   base_path="/dicom-web")

# streaming — bounded memory over a large study
import io
for part10 in dicomweb.iter_study("http://localhost:8042", study_uid, base_path="/dicom-web"):
    ds = pydcm.dcmread(io.BytesIO(part10))

Other retrieve shapes: retrieve_series, retrieve_frames, retrieve_*_metadata, retrieve_rendered, retrieve_bulkdata, and the streaming start_retrieve (writes parts to a directory off the GIL).

Requesting a transfer syntax

retrieve_study / retrieve_series / retrieve_instance / start_retrieve all take transfer_syntax=, which sets the WADO-RS Accept: …; transfer-syntax= parameter:

transfer_syntax What you get
omitted / "" (default) The server's default encoding. Per PS3.18 that is Explicit VR Little Endian for application/dicom (the server may transcode to it) — it is not guaranteed to be the stored syntax.
"*" The instances as stored, no transcoding — use this to retrieve compressed data verbatim.
a TS UID (e.g. "1.2.840.10008.1.2.4.50") That encoding; the server transcodes if it can, else falls back to its default.
# verbatim (keep J2K/JPEG as stored) — the no-transcode retrieve:
parts = dicomweb.retrieve_series(url, study_uid, series_uid, base_path="/dicom-web",
                                 transfer_syntax="*")

(For DIMSE C-GET the equivalent "as-stored" is the default — see the DIMSE how-to: the SCU offers the full transfer-syntax set, so a non-transcoding server returns each instance in its stored syntax without you naming one.)

Store (STOW-RS)

result = dicomweb.store_instances(
    "http://localhost:8042",
    [open("ct.dcm", "rb").read()],
    base_path="/dicom-web",
)
# result → {"status": 200, "stored": [...], "failed": [...]}

Delete

dicomweb.delete_instance("http://localhost:8042", study_uid, series_uid, sop_uid, base_path="/dicom-web")
dicomweb.delete_series("http://localhost:8042", study_uid, series_uid, base_path="/dicom-web")
dicomweb.delete_study("http://localhost:8042", study_uid, base_path="/dicom-web")

Worklist (UPS-RS)

Unified Procedure Step over the wire — create, query, claim and update work items:

S, BP = "http://localhost:8080", "/dcm4chee-arc/aets/DCM4CHEE/rs"
wi = dicomweb.create_workitem(S, workitem_dataset, base_path=BP)
open_items = dicomweb.search_workitems(S, matches={"ProcedureStepState": "SCHEDULED"}, base_path=BP)
dicomweb.change_workitem_state(S, workitem_uid, "IN PROGRESS", transaction_uid, base_path=BP)
dicomweb.update_workitem(S, workitem_uid, changes, transaction_uid, base_path=BP)

Plus retrieve_workitem, request_cancel_workitem, and subscribe_workitem / unsubscribe_workitem for event subscriptions.