Sorry I'm new to being a developer and little to none experience with Github - I'll try to follow the developer's guide when I get some time.
But I rarely do changes to source code, so not sure I will get to it.
I implemented a "mean" version of my ROIs instead of median for CT phantom analysis.
Honestly, I like median but my group had some dissenting voices and the quoted TG mentions "mean" and doesn't mention median :(
So I thought I'd share what I did.
Code changes:
FYI: I use virtual environment and analyzing gammex_phantom clinically.
Gammex code (Custom code for Gammex.py) was described in the forums (Thanks for that btw!!!)
and it inherits CheeseModule & CheesePhantomBase (cheese.py)
which inherits from CatPhanBase, CatPhanModule (ct.py)
which inherits from DiskROI (roi.py)
Lib\site-packages\pylinac\core\ROI.py (Maybe this is all that is needed for the actual mean to surface, but I made other changes below):
def pixel_value(self) -> float:
"""The mean pixel value of the ROI."""
masked_img = self.circle_mask()
return float(np.mean(masked_img))
Lib\site-packages\pylinac\ct.py (I think this is for CatPhan504 cause I started using this for testing but then later I noticed my CheesePhantom/Gammex didn't see the change to mean so I went to ROI.py):
def rois_to_results(dict_mapping: dict[str, DiskROI]) -> dict[str, ROIResult]:
"""Converts a dict of HUDiskROIs to a dict of ROIResults. This is for dumping to simple data formats for results_data and RadMachine"""
flat_dict = {}
for name, roi in dict_mapping.items():
flat_dict[name] = ROIResult(
name=name,
value=np.mean(roi.pixel_values), # Correctly calculates the mean from the pixel array
stdev=roi.std,
difference=roi.value_diff if hasattr(roi, "value_diff") else None,
nominal_value=roi.nominal_val if hasattr(roi, "nominal_val") else None,
passed=roi.passed if hasattr(roi, "passed") else None,
)
return flat_dict
and:
class ROIResult(BaseModel):
name: str = Field(description="The region the ROI was sampled from.")
value: float = Field(description="The measured HU value. NOTE: MODIFIED to MEAN not MEDIAN")
stdev: float = Field(description="The pixel value standard deviation of the ROI.")
difference: float | None = Field(description="The difference between the measured and nominal values.")
nominal_value: float | None = Field(description="The nominal HU value.")
passed: bool | None = Field(description=" Whether the ROI passed.")
Lib\site-packages\pylinac\cheese.py
def results(self, as_list: bool = False) -> str | list[str]:
"""Return the results of the analysis as a string. Use with print().
Parameters
----------
as_list : bool
Whether to return as a list of strings vs single string. Pretty much for internal usage.
"""
results = [
f" - {self.model} Phantom Analysis - ",
" - HU Module - ",
]
results += [
f"ROI {name} Mean: {roi.pixel_value:.2f}, stdev: {roi.std:.1f}"
for name, roi in self.module.rois.items()
]
if as_list:
return results
else:
return "\n".join(results)
and
def _quaac_datapoints(self) -> dict[str, QuaacDatum]:
results_data = self.results_data(as_dict=True)
data = {}
data["Phantom roll"] = QuaacDatum(
value=results_data["phantom_roll"],
unit="degrees",
)
for roi_num, roi_data in results_data["rois"].items():
data[f"ROI {roi_num}"] = QuaacDatum(
value=roi_data["mean"],
unit="HU",
)
return data
BTW - the code's roll uncertainty is pretty impressive. I intentionally did an angular sensitivity via software to see how the code would find ROIs and was impressed when it already has some robust rotational analysis within 10 degrees I think.