- the RTDOSE instance references the RTPLAN
- the RTPLAN references the RTSTRUCT
- the RTSTRUCT references a set of slices
If you’re interested, here’s a script that allows you to display the chain of references.
How the script works:
You need pass it the ID (either Orthanc ID or SOP Instance UID) of an RTDOSE, RTPLAN or RTSTRUCT instance, and it will display the various referenced instances as well as the Orthanc URL (for ease of use : it can be clicked on in the terminal)
Beware that if you pass the RTPLAN, it will not LOOK for the RTDOSE that is associated (if applicable) and it will only display RTSTRUCT and slices.
Conversely. if you pass the RTSTRUCT ID, it will only display the referenced slices, not the RTPLAN.
This script requires click
and requests
If you don’t know how to run Python scripts, just ask for help.
"""
This tool will dump the objects referenced from an RTDOSE, RTPLAN or RTSTRUCT file
"""
from dataclasses import dataclass
import re
import sys
import click
import requests
from requests.auth import HTTPBasicAuth
from ..common import click_pathlib
orthanc_id_to_tags = {}
def get_orthanc_uid(dicom_uid, cli_params):
"""Retrieve Orthanc UID from SOPInstanceUID."""
response = requests.post(f"{cli_params.orthanc_root_url}/tools/lookup", auth=cli_params.auth_obj, data=dicom_uid)
response.raise_for_status()
results = response.json()
if results:
return results[0]["ID"]
return None
def get_instance_tags(orthanc_id: str, cli_params):
"""Get the tags of an instance from Orthanc."""
if orthanc_id in orthanc_id_to_tags:
return orthanc_id_to_tags[orthanc_id]
response = requests.get(f"{cli_params.orthanc_root_url}/instances/{orthanc_id}/simplified-tags", auth=cli_params.auth_obj)
response.raise_for_status()
tags = response.json()
orthanc_id_to_tags[orthanc_id] = tags
return tags
def identify_id_type(id_str):
sop_instance_uid_pattern = r'^\d+(\.\d+)+$'
orthanc_id_pattern = r'^[0-9a-fA-F\-]+$'
if re.match(sop_instance_uid_pattern, id_str):
return "SOP Instance UID"
elif re.match(orthanc_id_pattern, id_str):
return "Orthanc ID"
else:
return "Unknown"
def get_sop_instance_uid(orthanc_id, cli_params):
return get_instance_tags(orthanc_id, cli_params).get("SOPInstanceUID", None)
def get_orthanc_id(any_id, cli_params):
# find out if the id is a SOPInstanceUID or an Orthanc ID
id_type = identify_id_type(any_id)
# if it's a SOP Instance UID, we need to find the Orthanc ID
if id_type == "SOP Instance UID":
orthanc_id = get_orthanc_uid(any_id, cli_params)
if orthanc_id is None:
print(f"Could not find Orthanc ID for SOPInstanceUID {any_id}")
return 1
else:
orthanc_id = any_id
return orthanc_id
def dump_instance_info(type_str, orthanc_id, cli_params):
sop_instance_uid = get_sop_instance_uid(orthanc_id, cli_params)
orthanc_oe1_url = f"{cli_params.orthanc_root_url}/app/explorer.html#instance?uuid={orthanc_id}"
print(f"{type_str}:")
print(f" SOPInstanceUID: {sop_instance_uid}")
print(f" Orthanc URL: {orthanc_oe1_url}")
def process_rtdose(dose_id, cli_params):
dose_orthanc_id = get_orthanc_id(dose_id, cli_params)
dump_instance_info("RTDOSE", dose_orthanc_id, cli_params)
ref_rt_plan = get_instance_tags(dose_orthanc_id, cli_params).get("ReferencedRTPlanSequence", [])
if ref_rt_plan:
for rt_plan in ref_rt_plan:
rt_plan_orthanc_id = rt_plan.get("ReferencedSOPInstanceUID")
process_rtplan(rt_plan_orthanc_id, cli_params)
def process_rtplan(plan_id, cli_params):
plan_orthanc_id = get_orthanc_id(plan_id, cli_params)
dump_instance_info("RTPLAN", plan_orthanc_id, cli_params)
ref_rt_struct = get_instance_tags(plan_orthanc_id, cli_params).get("ReferencedStructureSetSequence", [])
if ref_rt_struct:
for rt_struct in ref_rt_struct:
rt_struct_orthanc_id = rt_struct.get("ReferencedSOPInstanceUID")
process_rtstruct(rt_struct_orthanc_id, cli_params)
def process_rtstruct(struct_id, cli_params):
struct_orthanc_id = get_orthanc_id(struct_id, cli_params)
dump_instance_info("RTSTRUCT", struct_orthanc_id, cli_params)
roi_contour_seq = get_instance_tags(struct_orthanc_id, cli_params).get("ROIContourSequence", [])
if roi_contour_seq:
if cli_params.dump_individual_instances:
for roi_contour in roi_contour_seq:
contour_seq = roi_contour.get("ContourSequence", [])
for contour in contour_seq:
contour_image_seq = contour.get("ContourImageSequence", [])
if contour_image_seq:
for contour_image in contour_image_seq:
ref_instance_uid = contour_image.get("ReferencedSOPInstanceUID")
ref_orthanc_id = get_orthanc_id(ref_instance_uid, cli_params)
print(f" Referenced Instance:")
orthanc_oe1_url = f"{cli_params.orthanc_root_url}/app/explorer.html#instance?uuid={ref_orthanc_id}"
print(f" SOPInstanceUID: {ref_instance_uid}")
print(f" Orthanc URL: {orthanc_oe1_url}")
else:
series_instance_uids = set()
for roi_contour in roi_contour_seq:
contour_seq = roi_contour.get("ContourSequence", [])
for contour in contour_seq:
contour_image_seq = contour.get("ContourImageSequence", [])
# we simply collect the series
for contour_image in contour_image_seq:
ref_instance_uid = contour_image.get("ReferencedSOPInstanceUID")
ref_orthanc_id = get_orthanc_id(ref_instance_uid, cli_params)
# find the series
series_instance_uid = get_instance_tags(ref_orthanc_id, cli_params).get("SeriesInstanceUID", [])
series_instance_uids.add(series_instance_uid)
for series_instance_uid in series_instance_uids:
series_orthanc_id = get_orthanc_uid(series_instance_uid, cli_params)
print(f" Referenced Series:")
orthanc_oe1_url = f"{cli_params.orthanc_root_url}/app/explorer.html#series?uuid={series_orthanc_id}"
print(f" SeriesInstanceUID: {series_instance_uid}")
print(f" Orthanc URL: {orthanc_oe1_url}")
@dataclass
class CliParams:
orthanc_root_url: str
auth_obj: HTTPBasicAuth
dump_individual_instances: bool
def traverse_resources(initial_id, orthanc_root_url, auth_string, dump_individual_instances):
# default Orthanc URL
if orthanc_root_url is None:
orthanc_root_url = "http://localhost:8042"
# if orthanc_root_url has a trailing slash, remove it
if orthanc_root_url[-1] == "/":
orthanc_root_url = orthanc_root_url[:-1]
# Parse the auth_string if provided
auth = None
if auth_string:
user, password = auth_string.split(':')
auth = HTTPBasicAuth(user, password)
cli_params = CliParams(orthanc_root_url, auth, dump_individual_instances)
orthanc_id = get_orthanc_id(initial_id, cli_params)
# now find out if it's an RTDOSE, RTPLAN or RTSTRUCT
response = requests.get(f"{cli_params.orthanc_root_url}/instances/{orthanc_id}/simplified-tags", auth=auth)
# add error handling
modality = response.json().get("Modality", None)
if modality is None:
print(f"Could not find modality for Orthanc ID {orthanc_id}")
return 1
if modality == "RTDOSE":
process_rtdose(orthanc_id, cli_params)
elif modality == "RTPLAN":
process_rtplan(orthanc_id, cli_params)
elif modality == "RTSTRUCT":
process_rtstruct(orthanc_id, cli_params)
else:
print(f"Modality {modality} not supported")
return 1
@click.command()
@click.argument("initial_id", type=str)
@click.option(
"-o",
"--orthanc-url",
"orthanc_url",
required=False,
type=str,
help="The Orthanc root url, like https://my.orthanc.host:8442/. If not set, will default to localhost:8042",
)
@click.option(
"-a",
"--auth",
"auth",
required=False,
type=str,
help="The auth string like user:password",
)
@click.option(
"-i",
"--individual-instances",
"dump_individual_instances",
is_flag=True,
help="If set, every referenced instance will be reported",
)
def click_main(initial_id, orthanc_url, auth, dump_individual_instances) -> int:
"""Picks an UID (SOPInstanceUID or Orthanc ID) to an RTDOSE, RTPLAN or RTSTRUCT, and display all the referenced resources"""
traverse_resources(initial_id, orthanc_url, auth, dump_individual_instances)
return 0
if __name__ == "__main__":
click_main()