ImagingDataCommons / highdicom

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

Support of image-level qualitative evaluation in TID 1501 #164

Closed fedorov closed 2 years ago

fedorov commented 2 years ago

TID 1501 allows to capture qualitative evaluations assigned per image (without specifying any annotation) - see rows 10b/11b in https://dicom.nema.org/medical/dicom/current/output/chtml/part16/chapter_A.html#sect_TID_1501.

This does not appear to be possible using highdicom - I don't see how I could specify image in the constructor of TID1501: https://github.com/herrmannlab/highdicom/blob/master/src/highdicom/sr/templates.py#L2420. Am I correct, or I am missing something?

hackermd commented 2 years ago

We have assumed so far that measurements and qualitative evaluations provided via TID 1501 would apply to all images referenced in the SR document via either the Current Requested Procedure Evidence Sequence or the Pertinent Other Evidence Sequence.

We explicitly did not allow users to reference a SCOORD (row 10c) or SCOORD3D (row 10e) content item via TID 1501 (highdicom.sr.MeasurementsAndQualitativeEvaluations), because our opinion is that TID 1410 (highdicom.sr.PlanarROIMeasurementsAndQualitativeEvaluations) or TID 1411 (highdicom.sr.VolumetricROIMeasurementsAndQualitativeEvaluations) should be used for that purpose.

We could allow users to reference one or more IMAGE (row 10b) content items. I can see that this could be useful for classifying individual image frames of a multi-frame image or a single-frame image of a series.

CPBridge commented 2 years ago

I've actually wondered about this myself in the past but didn't notice that this was an option in the template. I would vote strongly in favour of adding this (for IMAGE at least, I agree that other TIDs should be used for ROIs). It would be relatively straightforward. It would also be nice to make this searchable in the deocding API via an additional option in MeasurementReport.get_image_measurement_groups(). I'm happy to work on this at some point.

@fedorov if you have an immediate need, it's simple to do this yourself before a change is made to the library by just appending your own ImageContentItem to the template:

import highdicom as hd

meas_group = hd.sr.MeasurementsAndQualitativeEvaluations(...)

source_image = hd.sr.ImageContentItem(
    name=hd.sr.CodedConcept('260753009', 'SCT', 'Source'),
    referenced_sop_class_uid=...,
    referenced_sop_instance_uid=...,
    relationship_type=hd.sr.RelationshipTypeValue.CONTAINS
)

meas_group.append(source_image)
fedorov commented 2 years ago

Thank you guys, this is very helpful. Indeed, this is a for a very specific immediate need use case to encode the results produced by this tool: https://github.com/mic-dkfz/bodypartregression. But it will also be applicable for this one that's also relevant to IDC: https://wiki.cancerimagingarchive.net/pages/viewpage.action?pageId=80969742 (image-level COVID findings).

fedorov commented 2 years ago

A related issue is that currently tracking ID/UID are mandatory for MeasurementsAndQualitativeEvaluations, but they are definitely not required by TID 1501, and for the situation described in this issue I do not think they should be required under any circumstances. There is no annotation to track.

hackermd commented 2 years ago

A related issue is that currently tracking ID/UID are mandatory for MeasurementsAndQualitativeEvaluations, but they are definitely not required by TID 1501, and for the situation described in this issue I do not think they should be required under any circumstances. There is no annotation to track.

This was a design decision. I think one should always provide identifiers, even if there is no intention to track (at the time of creation)

fedorov commented 2 years ago

Ok, I understand. To clarify, it's not that there is no intention to track, it is that it is not clear what those identifiers would refer to. There will be nothing in the container other than the image and the code content items.

fedorov commented 2 years ago

@CPBridge I followed your advice re workaround, and the IMAGE content item is added outside of the container. Is there something I am not doing correctly?

for i in range(10):
  measurements_group = MeasurementsAndQualitativeEvaluations(
                tracking_identifier=TrackingIdentifier(
                    uid=generate_uid(),
                    identifier=str(f"Tracking ID {i}")
                ),
                qualitative_evaluations=[qualitative_evaluation]
            )

  reference_dcm_file = os.path.join(os.environ["DOWNLOAD_DEST"], os.listdir(os.environ["DOWNLOAD_DEST"])[i])
  image_dataset = dcmread(reference_dcm_file)
  evidence.append(image_dataset)

  # which image is annotated
  source_image = hd.sr.ImageContentItem(
      name=hd.sr.CodedConcept('260753009', 'SCT', 'Source'),
      referenced_sop_class_uid=image_dataset.SOPClassUID,
      referenced_sop_instance_uid=image_dataset.SOPInstanceUID,
      relationship_type=RelationshipTypeValues.CONTAINS
  )

  measurements_group.append(source_image)

  imaging_measurements.append(
    measurements_group
          )

measurement_report = MeasurementReport(
    observation_context=observation_context,
    procedure_reported=procedure_code,
    imaging_measurements=imaging_measurements
)

Result:

  <contains CONTAINER:(,,"Imaging Measurements")=CONTINUOUS>
    <contains CONTAINER:(,,"Measurement Group")=CONTINUOUS>
      <has obs context TEXT:(,,"Tracking Identifier")="Tracking ID 0">
      <has obs context UIDREF:(,,"Tracking Unique Identifier")="1.2.826.0.1.3680043.8.498.10832312462833275132331249495185225587">
      <contains CODE:(,,"Body part examined")=(39607008,SCT,"Lung")>
    <contains IMAGE:(,,"Source")=(CT image,)>

So I am thinking to instead manually populate the container as follows:

  container = hd.sr.ContainerContentItem(hd.sr.CodedConcept("125007","DCM","Measurement Group"), relationship_type=RelationshipTypeValues.CONTAINS)
  container_content = hd.sr.ContentSequence()
  container_content.append(source_image)

  finding_type_item = CodeContentItem(
                  name = CodedConcept(
                  value="temp1",
                  meaning="Body part examined",
                  scheme_designator="99andrey"
                ), 
                value = CodedConcept(
                  value="39607008",
                  meaning="Lung",
                  scheme_designator="SCT"
                ),
                relationship_type=RelationshipTypeValues.CONTAINS
              )
  container_content.append(finding_type_item)

  container.ContentSequence = container_content

  measurements_group.append(container)

This seems to work as expected:

    <contains CONTAINER:(,,"Measurement Group")=CONTINUOUS>
      <contains IMAGE:(,,"Source")=(CT image,)>
      <contains CODE:(,,"Body part examined")=(39607008,SCT,"Lung")>
fedorov commented 2 years ago

This seems to work as expected

Except this is not going to work, since MeasurementReport would not accept my list of containers in place of list of MeasurementsAndQualitativeEvaluations, so I am not sure how to proceed.

CPBridge commented 2 years ago

HI @fedorov my apologies: I made a mistake in my initial snippet, you should append the new ImageContentItem (source_image) to the ContentSequence of the MeasurementsAndQualitativeEvaluations, and not the object itself.

I.e. adapting your first snippet, change this:

measurements_group.append(source_image)

to this:

measurements_group.ContentSequence.append(source_image)

However I have already addressed this issue in pull request #169, so that you can do everything through the constructors as you would expect. So I would suggest that you use that branch instead. @hackermd approved that PR but I'm not sure why he didn't merge it to master yet, I think we both agreed it's good to go as is.

On that branch, you can do this instead:

for i in range(10):
    reference_dcm_file = os.path.join(os.environ["DOWNLOAD_DEST"], os.listdir(os.environ["DOWNLOAD_DEST"])[i])
    image_dataset = dcmread(reference_dcm_file)
    evidence.append(image_dataset)

    # There is a new object that encapsulates the source image as a content
    # item with the correct codes and relationship type, and can be
    # automatically created from the source dataset
    source_image = hd.sr.SourceImageForMeasurementGroup.from_source_image(image_dataset)

    # Then just pass this as the `source_images` parameter of the constructor
    measurements_group = MeasurementsAndQualitativeEvaluations(
        tracking_identifier=TrackingIdentifier(
            uid=hd.UID(),
            identifier=str(f"Tracking ID {i}")
        ),
        qualitative_evaluations=[qualitative_evaluation],
        source_images=[source_image]
    )

    imaging_measurements.append(measurements_group)

measurement_report = MeasurementReport(
    observation_context=observation_context,
    procedure_reported=procedure_code,
    imaging_measurements=imaging_measurements
)
hackermd commented 2 years ago

169 has meanwhile been merged.

fedorov commented 2 years ago

Thank you for your help with this. I wanted to try it out, but am experiencing issues with importing packages while installing using setup.py - import pydicom is failing for me, here's the notebook with the steps: https://colab.research.google.com/drive/1OHzeZY03gG7PdJTK85DOAaPC-fp2YwHA?usp=sharing. Am I supposed to use python setup.py install to install the version that does not have a release, or there is some other process?

CPBridge commented 2 years ago

Try pip install . from the root directory of the cloned repo, rather than directly invoking setup.py

fedorov commented 2 years ago

Thank you! This worked. Is this something that everyone else other than me is expected to know, or maybe this can be added to the documentation?

fedorov commented 2 years ago

Resolved by #169

CPBridge commented 2 years ago

In fact it's already there ;)

https://highdicom.readthedocs.io/en/latest/installation.html

It is also a standard way of setting up Python packages these days so that they play nicely with pip

hackermd commented 2 years ago

@fedorov version 0.17.0 is now available on PyPI: https://pypi.org/project/highdicom/0.17.0/