ImagingDataCommons / highdicom

High-level DICOM abstractions for the Python programming language
https://highdicom.readthedocs.io
MIT License
177 stars 37 forks source link

Add segments to highdicom Segmentation object after its construction #299

Open ramyayyagari opened 2 months ago

ramyayyagari commented 2 months ago

From my understanding, the current version of highdicom doesn't allow for updating Segmentation objects after they are constructed. A previous version of highdicom (< 0.8.0) included the add_segments feature which allowed for adding segments to existing Segmentation objects, but this has since been deprecated.

I would like to update my Segmentation objects after they have been constructed. Since these objects are immutable, how do I work around this?

CPBridge commented 2 months ago

Hi @ramyayyagari thanks for the question.

Unfortunately adding segments to an existing seg gets quite tricky because of the way they are structured internally. If at all possible, therefore, I would restructure your process to enable creating a segmentation only once with multiple segments as in this section of the documentation.

However if for whatever reason that simply isn't possible, your best option is to simply create a new segmentation object using the normal constructor and copy everything from the existing object. Assuming that you have the original source images (i.e. the image files to which the segmentation applies), this will be a little verbose but straightforward.

Assuming you are working with a series of source instances as in the typical radiology case:

import pydicom
import highdicom as hd

# Read in the existing segmentation
old_seg = hd.seg.segread("old_segmentation.dcm")

# Read in the source images
source_image_paths = ["im1.dcm", "im2.dcm", "im3.dcm"]
source_images = [pydicom.dcmread(im) for im in source_image_paths]

# Get the segmentation mask from the original segmentation (note that if your source images are multiframe instances you would use the get_pixels_by_source_frame method instead)
source_sop_uids = [im.SOPInstanceUID for im in source_images]
old_mask = old_seg.get_pixels_by_source_instance(source_sop_uids)

# Concatenate the new mask to the old mask
new_mask = np.zeros(...) # shape slices x rows x columns x segments
combined_mask = np.concatenate([old_mask, new_mask], axis=3)

# Create a segment description for the new segment(s)
old_segment_descriptions = old_seg.SegmentSequence
new_segment_descriptions = [
    hd.seg.SegmentDescription(
        segment_number=len(old_segment_descriptions) + 1,
        segment_label='liver',
        segmented_property_category=codes.SCT.Organ,
        segmented_property_type=codes.SCT.Liver,
        algorithm_type=hd.seg.SegmentAlgorithmTypeValues.MANUAL,
    ),
   ...
]
combined_descriptions = old_segment_descriptions + new_segment_descriptions

# Create the new segmentation object combining old and new
combined_seg = hd.seg.Segmentation(
    source_images=source_images,
    pixel_array=combined_mask,
    segmentation_type=old_seg.segmentation_type,
    segment_descriptions=combined_descriptions,
    series_instance_uid=hd.UID(),
    series_number=old_seg.SeriesNumber,
    sop_instance_uid=hd.UID(),
    instance_number=old_seg.InstanceNumber,
    manufacturer=old_seg.Manufacturer,
    manufacturer_model_name=old_seg.ManufacturerModelName,
    software_versions=old_seg.SoftwareVersions,
    device_serial_number=old_seg.DeviceSerialNumber,
    ...  # there may be other metadata you need to copy
)

If you no longer have access to the source images, this gets a bit more fiddly since you need to deduce the information about them from the existing segmentation. This is possible but quite inconvenient at the moment. I actually plan to add some utility methods to do this in the near future. Further, if your segmentation frames are not spatially aligned 1:1 with your source frames, you will also need to copy over the geometry from the source image. I omitted this for the sake of clarity but can help if needed.

Note that I would generally recommend against copying the SOPInstanceUID and SeriesInstanceUID from the original image, since according to the standard these UIDs should refer only to a single immutable object. Therefore the example above generates new UIDs for the new seg. But in certain situations you may want to copy these across so that it really appears like you have added segments to an existing segmentation.

It probably would be good for us to provide an easier way to do this in the future...

ramyayyagari commented 1 month ago

Thanks for clarifying! Looking forward to the utility methods for accessing source images from previous segmentations. Currently I do not have access to the source images, so I am loading these images every time I create a new Segmentation, which is taking up time. Thanks again!

CPBridge commented 1 month ago

I'll leave this issue open as a reminder to implement the utility method