Plugin setup and Orthanc Explorer 2 BodyPartExamined Tag on Series level

I developed a plugin that parses the ImageComments tage of an incoming instance and modifies the incoming dcm BodyPartExamined Tag depending on the comment and then uploads the modified dcm instance. Lastly, if the modified instance is uploaded correctly the original DICOM instance is deleted.

import orthanc
import sys

# print("sys.path before modification: " + ", ".join(sys.path))
# Print contents of /tmp/.venv/
import os
sys.path = ["/etc/orthanc", "/usr/lib/python3.7", "/usr/lib/python3.7/lib-dynload", "/usr/local/lib/python3.7/dist-packages", '/usr/lib/python3/dist-packages', "/tmp/.venv/lib64/python3.7/site-packages"]

# print("sys.path after modification: " + ", ".join(sys.path))
import pydicom
import json
import io

def get_location(comment):
    if len(comment) == 1:
        comment = comment + " "
    if comment.startswith("l "):
        return "Left Ovary"
    elif comment.startswith("r "):
        return "Right Ovary"
    elif comment.startswith("u "):
        return "Uterus"
    else:
        return "Unknown"

def set_instance_location(instance_id, location):
    if location in ["Left Ovary", "Right Ovary"]:
        if location == "Right Ovary":
            anatomic_region = {
                "CodeValue": "280123002",
                "CodingSchemeDesignator": "SCT",
                "CodeMeaning": "Right Ovary",
            }
        else:
            anatomic_region = {
                "CodeValue": "280124008",
                "CodingSchemeDesignator": "SCT",
                "CodeMeaning": "Left Ovary",
            }
    elif location == "Uterus":
        anatomic_region = {
            "CodeValue": "181452004",
            "CodingSchemeDesignator": "SCT",
            "CodeMeaning": "Uterus",
        }
    else:
        return  # Don't modify if location is Unknown

    replace_tag_dict = {
        "BodyPartExamined": location,
        "AnatomicRegionSequence": [anatomic_region],
    }
    set_instance_tags(instance_id, replace_tag_dict)

def set_instance_tags(instance_id, replace_tag_dict):
    # Modify the instance
    modified_instance = orthanc.RestApiPost(f'/instances/{instance_id}/modify', json.dumps({"Replace": replace_tag_dict}))
    
    # Get the modified instance
    dcm = pydicom.dcmread(io.BytesIO(modified_instance))

    print(f"Instance {instance_id} modified with new metadata: {dcm.BodyPartExamined}")
    
    # Upload the modified instance
    new_instance_id = upload_instance(dcm)
    if new_instance_id:
        delete_instance(instance_id)

def upload_instance(dcm):
    dcm_bytes = io.BytesIO()
    dcm.save_as(dcm_bytes)
    new_instance = orthanc.RestApiPost('/instances', dcm_bytes.getvalue())
    new_instance_id = json.loads(new_instance)['ID']
    print(f"Instance successfully uploaded: /instances/{new_instance_id}")
    return new_instance_id

def delete_instance(instance_id):
    orthanc.RestApiDelete(f'/instances/{instance_id}')
    print(f"Instance with orthanc ID: {instance_id} at /instances/{instance_id} successfully deleted.")

def OnStoredInstance(dicom, instanceId):
    print(f'New instance received: {instanceId}')
    #print(dicom.GetInstanceOrigin())
    print("Number of frames: ", dicom.GetInstanceFramesCount())
    simplified_dcm_json = dicom.GetInstanceSimplifiedJson()
    simplified_dcm_dict = json.loads(simplified_dcm_json)
    # orthanc.OrthancPluginFreeString(simplified_dcm_json)
    advanced_dcm_json = dicom.GetInstanceAdvancedJson(3,0,20)
    advanced_dcm_dict = json.loads(advanced_dcm_json)
    # orthanc.OrthancPluginFreeString(advanced_dcm_json)
    # Check if BodyPartExamined is present in simplified and advanced JSON
    print(f"BodyPartExamined - Simplified: {simplified_dcm_dict.get('BodyPartExamined', '')}, Advanced: {advanced_dcm_dict.get('BodyPartExamined', '')}")
    # Check if ImageComments is present in simplified and advanced JSON
    print(f"ImageComments - Simplified: {simplified_dcm_dict.get('ImageComments', '')}, Advanced: {advanced_dcm_dict.get('ImageComments', '')}")
    # orthanc.OrthancPluginFreeString(new_uuid)
    # Generate new UUID
    new_uuid = orthanc.GenerateUuid()
    print("New UUID: ", new_uuid)
    # orthanc.OrthancPluginFreeString(new_uuid)
    
    # Check BodyPartExamined field
    body_part_examined = advanced_dcm_dict.get('BodyPartExamined', '')
    print(f"BodyPartExamined: {body_part_examined}")
    
    if not body_part_examined:
        # If BodyPartExamined is empty or doesn't exist, check ImageComments
        image_comments = advanced_dcm_dict.get('ImageComments', '')
        print(f"ImageComments: {image_comments}")
        if image_comments:
            location = get_location(image_comments)
            print(f"Location: {location}")
            if location != "Unknown":
                set_instance_location(instanceId, location)
orthanc.RegisterOnStoredInstanceCallback(OnStoredInstance)

The parsing is done from the orthanc plugin functions while the modification, upload and deletion is done using the Orthanc plugin REST API functions. Is there a easier or more efficient way to do this without having to reupload, check and delete?

I was also not able to use the orthanc.OrthancPluginFreeString(new_uuid) function correctly (it didnt find the fucntion) Im not sure what I am doing wrong for this.

I also noticed the BodyPartExamined Tag is on the series level in the Orthanc Explorer 2. The BodyPartExamined tag is however normally a instance level tag. When I upload a new instance with a new series (and the tag is added using my plugin). The Series level BodyPartExamined Tag in the explorer stays empty, only the instance level view shows the correct tag value.

What was the intention here?
For my research project im receiving images from 3 different body parts and was debating to split them into separate series when they are received according to their BodyPartExamined and therefore indirectly depending on the ImageComments.

I would then like to automatically segment them using a python segmentation pipeline I have developed. The pipeline requires python 3.10 and the system level python of the orthanc docker image is only 3.7. Would you recommend that I set up a separate python webservice listening on another port that receives the new dicom images and returns the segmentation instance to the orthanc docker image?

Hello,

This is how things have to be done.

You don’t have to explicitly call this function in Python plugins. The strings are automatically freed for you. Check out the source code of the orthanc.GenerateUuid() function and the associated OrthancString class.

The “Body Part Examined” (0018,0015) tag is a series-level tag, as it is part of the “General Series Module”, according to the DICOM specification. Using this tag is thus inappropriate in your context, where you seem to want to store instance-level information.

If you need to store custom, task-specific information about individual instances, the “Image Comments” tag is indeed commonly used. Medical imaging vendors often define private tags to this end, though this practice is certainly not encouraged.

If your workflow is entirely built on Orthanc, you could also consider the use of metadata, which could allow you to totally avoid the modification of DICOM instances.

This is certainly not what I would personally recommend, as PyTorch and TensorFlow can directly be invoked by Python plugins, right from the Orthanc process. This is precisely the topic of the scientific paper Integrated and Interoperable Platform for Detecting Masses on Mammograms that I will present next week at Medical Informatics Europe (MIE 2024).

Kind Regards,
Sébastien-