Volume and fractions

Background. The simplest habitat number is how much of the tumour each habitat occupies. Volume fractions let you ask, for example, whether a larger share of one habitat goes with a worse outcome.

Purpose. You get a table with one row per habitat (volume fraction, number of separate pieces, size of the largest piece), an overlay of the habitat map, and a bar chart of the fractions.

Key terms.

  • habitat – a sub-region inside the tumour (the ROI) whose voxels behave alike across the input images; HABIT paints each ROI voxel with a habitat id (1, 2, 3, …).

  • volume fraction – voxels of one habitat divided by all non-background voxels of the map; fractions sum to 1.

  • region – one face-connected piece of a habitat; num_regions counts the pieces and largest_region_voxels is the size of the biggest one.

  • one-step habitats – clustered inside this one subject; see Defining habitats inside each subject.

Atomic volume metrics from a habitat label map: habitat_volume_fractions() and habitat_region_stats().

One-step habitats give a map to quantify. sphinx_gallery_thumbnail_number = 2

from pathlib import Path

import matplotlib.pyplot as plt
import pandas as pd

from habit.contracts import cohort_from_directory
from habit.datasets import fetch_demo
from habit.kernels import habitat_region_stats, habitat_volume_fractions
from habit.recipes import one_step_habitat
from habit.viz import plot_habitat_overlay, plot_habitat_volume_fractions

DATA = fetch_demo()
MODALITIES = ("LAP",)
ROI = "LAP"
cohort = cohort_from_directory(DATA, modalities=MODALITIES, roi=ROI)[:1]
result = one_step_habitat(
    modalities=MODALITIES, n_habitats=3, random_seed=0, roi=ROI
).fit_predict(cohort)
habitat_map = result.habitat_maps[0]
labels = habitat_map.label_array
HABIT demo data (cached)
DATA (preprocessed root): C:\Users\dongm\.habit_data\demo-data-v1\preprocessed

On-disk inventory of this folder:
  subjects (5): subj001, subj002, subj003, subj004, subj005
  image series: LAP, PVP, delay_3min, pre_contrast
  mask keys:    LAP, PVP, delay_3min, pre_contrast
  example image: images/subj001/delay_3min/WATER__BH_Ax_LAVA_Flex_3min_Series0012.nrrd
  example mask:  masks/subj001/delay_3min/WATER__BH_Ax_LAVA_Flex_10min_Series0017_mask.nrrd

Your own data must use the same folder tree (change IDs / series names):

  DATA/
    images/<subject_id>/<modality>/<one image file>
    masks/<subject_id>/<roi>/<one mask file>

Then load it with the same call the demos use:

  cohort = cohort_from_directory(DATA, modalities=("LAP",), roi="LAP")

Swap DATA / modalities / roi to match your tree. Mask key is often the
same as one image series (here LAP).

Cohort.map[_DefineAndLabelWithinSubject]:   0%|          | 0/1 [00:00<?, ?it/s]
Cohort.map[_DefineAndLabelWithinSubject]: 100%|██████████| 1/1 [00:01<00:00,  1.35s/it]
Cohort.map[_DefineAndLabelWithinSubject]: 100%|██████████| 1/1 [00:01<00:00,  1.35s/it]

Volume fractions and connected-component stats per habitat id. Collect the non-background ids actually present in this map.

ids = tuple(sorted({int(v) for v in labels.ravel() if int(v) != 0}))
# Share of the ROI per habitat: {habitat id: fraction in [0, 1]}.
frac = habitat_volume_fractions(labels, ids)
# Fragmentation per habitat: {habitat id: (num_regions, largest_region_voxels)}.
stats = habitat_region_stats(labels)
table = pd.DataFrame(
    [
        {
            "habitat": f"H{hid}",
            "volume_fraction": float(frac[hid]),
            "num_regions": int(stats[hid][0]),
            "largest_region_voxels": int(stats[hid][1]),
        }
        for hid in ids
    ]
)
print(table.to_string(index=False))
table

Path("out").mkdir(exist_ok=True)
fig = plot_habitat_overlay(
    cohort[0].image(ROI),
    habitat_map,
    title="Habitats (K=3)",
)
fig.savefig("out/volume_fractions_overlay.png", dpi=150, bbox_inches="tight")
fig_frac = plot_habitat_volume_fractions(frac)
fig_frac.savefig("out/volume_fractions_bar.png", dpi=150, bbox_inches="tight")
plt.show()
  • Habitats (K=3), Axis 0 (axial-like) @ 96, Axis 1 (coronal-like) @ 165, Axis 2 (sagittal-like) @ 71
  • Habitat volume fractions
habitat  volume_fraction  num_regions  largest_region_voxels
     H1         0.410791           29                  14183
     H2         0.289099           20                   9499
     H3         0.300110           19                   9063
F:\work\habit_project\habit\viz\habitat_overlay.py:806: UserWarning: Display geometry conflict: image/anatomy direction does not match mask/label direction. Using the mask/label direction so coronal/sagittal superior-up follows the labelled anatomy. Pass direction= to override.
  resolved_direction, resolved_spacing = resolve_display_geometry(

Total running time of the script: (0 minutes 6.660 seconds)

Gallery generated by Sphinx-Gallery