jrkerns / pylinac

An image analysis library for medical physics
https://pylinac.readthedocs.io/en/latest/
MIT License
157 stars 99 forks source link

Roll detection for "cheese" phantoms + additional dataset #525

Open mitch-1211 opened 2 weeks ago

mitch-1211 commented 2 weeks ago

Great to see some additional functionality added to the cheese module in f3ea44c, looking forward to 3.29!

I am looking at implementing a Gammex RMI 465 electron density phantom in Radmachine, initial testing being performed with pylinac directly. I have submitted a phantom dataset through the google form for your reference.

I have the below code currently, which includes ROI definition:

from pylinac.cheese import CheeseModule, CheesePhantomBase
from pylinac.ct import CatPhanBase
class GammexModule(CheeseModule):
    common_name = "RMI Gammex Phantom"
    inner_roi_dist_mm = 55
    outer_roi_dist_mm = 105
    periphary_roi_dist_mm = 142.5
    roi_radius_mm = 10
    roi_settings = {
        "1": {
            "angle": -90,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "2": {
            "angle": -45,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "3": {
            "angle": 0,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "4": {
            "angle": 45,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "5": {
            "angle": 90,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "6": {
            "angle": 135,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "7": {
            "angle": 180,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "8": {
            "angle": -135,
            "distance": inner_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "9": {
            "angle": -67.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "10": {
            "angle": -22.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "11": {
            "angle": 22.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "12": {
            "angle": 67.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "13": {
            "angle": 112.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "14": {
            "angle": 157.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "15": {
            "angle": -157.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "16": {
            "angle": -112.5,
            "distance": outer_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "17": {
            "angle": -90,
            "distance": periphary_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "18": {
            "angle": 0,
            "distance": periphary_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "19": {
            "angle": 90,
            "distance": periphary_roi_dist_mm,
            "radius": roi_radius_mm,
        },
        "20": {
            "angle": 180,
            "distance": periphary_roi_dist_mm,
            "radius": roi_radius_mm,
        }
    }

class GammexPhantom(CheesePhantomBase):
    model = "RMI Gammex Phantom"
    air_bubble_radius_mm = 10
    localization_radius = 105
    min_num_images = 2
    catphan_radius_mm = 165
    module_class = GammexModule
    module: GammexModule

densities = {
    "1": {"density": 1.081},
    "2": {"density": 1.707},
    "3": {"density": 1.039},
    "4": {"density": 0.292},
    "5": {"density": 1.000},
    "6": {"density": 1.000},
    "7": {"density": 1.142},
    "8": {"density": 0.945},
    "9": {"density": 1.116},
    "10": {"density": 1.099},
    "11": {"density": 0.895},
    "12": {"density": 1.473},
    "13": {"density": 1.000},
    "14": {"density": 0.438},
    "15": {"density": 1.285},
    "16": {"density": 1.000},
    "17": {"density": 0.980},
    "18": {"density": 1.050},
    "20": {"density": 1.147}
    }

image_folder = r"K:\ISS\MEDICAL PHYSICS SERVICES\Medical Physics\Radformation\Radmachine\Implementation\CT QA\Gammex images on foam block"
gammex = GammexPhantom(image_folder)

gammex.analyze(roi_config=densities)
print(gammex.results_data(as_dict=True)["phantom_roll"])
print(gammex.results())
gammex.plot_analyzed_image()
gammex.plot_density_curve()

Note I use the terminology inner_roi_dist_mm for the inner most ring of inserts, outer_roi_dist_mm for the next ring moving outwards and periphary_roi_dist_mm for the 4 HU plugs at the periphery.

We get a successful analysis using the above:

image

Something to note however: I've had to set localization_radius to be the distance corresponding to outer_roi_dist_mm i.e not the inner most ring. Using the inner most ring results in find_origin_slice raising ValueError("No slices were found that resembled the HU linearity module")

It's not necessarily and issue per-se, it's just something that people will probably run into. Is is possible there are too many inserts with HU close to background in the inner ring? We have three solid water inserts that are essentially background in that ring.

jrkerns commented 1 week ago

Yes, the localization of the phantom depends on there being both high and low HU ROIs. This was started back in the catphan days and has been extended (beyond its foreseen uses) to all the CT-like phantoms. You can also adjust the hu_origin_slice_variance parameter as well: https://pylinac.readthedocs.io/en/latest/cbct.html#localization-variance

A feature will be added to a future release that lets the user pass the slice number as an alternative to the localization as a fallback method.