03 — Scar and fibrosis¶
Turning intensities on a mesh into a scar score. This is the heart of what imatools
is for.
You will learn:
- how blood-pool statistics become a threshold (the
iirandmsdmethods) - how a threshold becomes a fibrosis score
- what
exclude_valuedoes, and whyimatools-scar scoresets it to 3
Self-contained: builds its own mesh and scalar field.
The idea¶
Scar shows up brighter than healthy myocardium on LGE. To decide how much is scar you need a cutoff, and that cutoff is derived from the blood pool — a region of known intensity in the same image. Two conventions:
| Method | id | Threshold | Reading |
|---|---|---|---|
iir |
1 | value * mean_bp |
image-intensity-ratio: a multiple of blood-pool mean |
msd |
2 | value * std_bp + mean_bp |
mean plus N standard deviations |
Same idea, different statistical footing. iir is the CEMRG default.
Setup¶
import numpy as np
import vtk
from imatools.core import mesh as cmesh
from imatools.core import scar as cscar
src = vtk.vtkSphereSource()
src.SetRadius(10.0)
src.SetThetaResolution(12)
src.SetPhiResolution(12)
src.Update()
sphere = src.GetOutput()
print(sphere.GetNumberOfCells(), "cells")
Thresholds from blood-pool stats¶
get_threshold takes a method id, not the string. get_scar_method maps the name.
mean_bp, std_bp = 100.0, 10.0
print("iir ->", cscar.get_scar_method("iir"))
print("msd ->", cscar.get_scar_method("msd"))
for name in ("iir", "msd"):
method = cscar.get_scar_method(name)
for value in (0.97, 1.2, 1.32):
th = cscar.get_threshold(method, value, mean_bp, std_bp)
print(f"{name} value={value:<5} -> threshold={th:7.3f}")
Note how differently they behave with mean_bp=100, std_bp=10: iir scales the mean, so
1.2 means "20% above blood pool" (120). msd adds standard deviations, so 1.2 means
"1.2 sigma above the mean" (112). The same number means different things — always say
which method you used.
A
value <= 0returns a threshold of0, not a negative cutoff.
A synthetic scalar field¶
Give the mesh intensities to score. Real data would come from projecting an LGE image onto the mesh; the maths downstream is identical.
rng = np.random.default_rng(42)
intensities = rng.uniform(0, 2, sphere.GetNumberOfCells()).astype(np.float32)
sphere.GetCellData().AddArray(cmesh.np_to_vtk_array(intensities, "scalars"))
sphere.GetCellData().SetActiveScalars("scalars")
print("intensity range:", round(float(intensities.min()), 3), "to", round(float(intensities.max()), 3))
Scoring¶
fibrosis_score is the fraction of the mesh at or above the threshold:
score = (cells >= th) / (cells not excluded)
It assumes the scalars are already normalised, and the comparison is >=.
for th in (0.5, 1.0, 1.5):
s = cmesh.fibrosis_score(sphere, th, type="cell")
print(f"threshold={th:<4} -> score={s:.4f} ({s*100:.1f}% of the mesh)")
exclude_value: dropping cells from the denominator¶
Some cells should not count at all — not scar, not healthy, just not applicable (outside
the region of interest). exclude_value removes them from the denominator rather than
scoring them as healthy.
The default 0 preserves the historical behaviour every existing caller relies on.
imatools-scar score passes CEMRGAPP_IGNORE instead — CemrgApp marks ignored cells 3.
print("CEMRGAPP_IGNORE =", cscar.CEMRGAPP_IGNORE)
plain = cmesh.fibrosis_score(sphere, 1.0, type="cell")
ignored = cmesh.fibrosis_score(sphere, 1.0, type="cell", exclude_value=cscar.CEMRGAPP_IGNORE)
print(f"exclude_value=0 (default): {plain:.4f}")
print(f"exclude_value=3 (CemrgApp): {ignored:.4f}")
They match here because this synthetic field contains no cells valued exactly 3 — nothing
gets excluded. On real CemrgApp output the two differ, and using the wrong one silently
shifts the score. Make it explicit.
The same thing from the CLI¶
imatools-scar score wraps all of the above — thresholds from custom blood-pool stats, one
score per value, no stats file needed:
imatools-scar score --mesh mesh.vtk \
--mean-bp 100 --stdev-bp 10 \
--method iir \
--value 0.97 1.2 1.32
It prints a value / threshold / score row per --value.
Recap¶
| Task | Call |
|---|---|
| Method name -> id | cscar.get_scar_method("iir") |
| Stats -> threshold | cscar.get_threshold(method, value, mean_bp, std_bp) |
| Threshold -> score | cmesh.fibrosis_score(msh, th, type="cell") |
| Exclude CemrgApp cells | ..., exclude_value=cscar.CEMRGAPP_IGNORE |
Next: 04_segmentation_labels — label maps on images rather than meshes.