Defining habitats inside each subject

Background. In the one-step design each tumour is clustered on its own voxels: no supervoxels, no pooling, and no model shared by the cohort.

Purpose. You get one private habitat model and one habitat map per subject, plus their volume fractions, drawn side by side.

When to use. You want to describe how heterogeneous each tumour is by itself (e.g. how many habitats, how fragmented). Skip it if habitat ids must mean the same thing across patients; use Defining habitats in two steps instead.

Key terms.

  • one-step – fit and assign run inside each subject, so every subject gets its own centroids.

  • label switching – independent clusterings number the same habitat differently, so ids must be matched before comparing subjects.

Input: one subject at a time. Output: a HabitatMap whose integer ids belong to that subject only. The stage list has no pool, so fit runs inside each subject instead of on the cohort. one_step_habitat(...) is a shortcut that builds the same stage list.

Habitat 1 in the first subject is not habitat 1 in the second. Match labels before comparing people: Matching habitat labels across subjects.

Load the cohort

Change DATA / MODALITIES / ROI to your preprocessed layout. sphinx_gallery_thumbnail_number = 1

from pathlib import Path

import matplotlib.pyplot as plt
import numpy as np

from habit.contracts import cohort_from_directory
from habit.datasets import fetch_demo
from habit.recipes import Study
from habit.spec import HabitatSpec, Spec, Stage
from habit.viz import plot_habitat_overlay

DATA = fetch_demo()
# Three DCE phases: unenhanced, arterial, and portal-venous.
MODALITIES = ("pre_contrast", "LAP", "PVP")
ROI = "LAP"
cohort = cohort_from_directory(DATA, modalities=MODALITIES, roi=ROI)[:3]
print(f"Cohort: {list(cohort.subject_ids)}")
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: ['subj001', 'subj002', 'subj003']

Fit a private model inside each subject

No partition and no pool: each subject’s ROI voxels are clustered on their own. Habitat 1 in the first subject is not habitat 1 in the second. There is no shared centroid.

spec = HabitatSpec(
    name="one_step",
    stages=(
        # extract: one intensity column per DCE phase, inside the ROI.
        Stage("extract", Spec("raw", {"modalities": list(MODALITIES), "roi": ROI})),
        # No partition and no pool: fit runs on this subject's voxels only.
        Stage("fit", Spec("kmeans", {"n_habitats": 3, "n_init": 10})),
        # assign: nearest centroid of THIS subject's model, not a shared one.
        Stage("assign", Spec("nearest_centroid")),
        Stage("volume", Spec("volume")),
    ),
    random_seed=0,
)
# Without pool, fit_predict keeps one model per subject (subject_models)
# instead of a single cohort habitat_model.
result = Study(spec).fit_predict(cohort)
print(f"Per-subject models: {list(result.subject_models)}")
for habitat_map in result.habitat_maps:
    present = sorted(
        int(v) for v in np.unique(habitat_map.label_array) if int(v) != 0
    )
    print(habitat_map.subject_id, present)
print(result.features.frame)
result.features.frame
Cohort.map[_DefineAndLabelWithinSubject]:   0%|          | 0/3 [00:00<?, ?it/s]
Cohort.map[_DefineAndLabelWithinSubject]:  33%|███▎      | 1/3 [00:01<00:02,  1.50s/it]
Cohort.map[_DefineAndLabelWithinSubject]:  67%|██████▋   | 2/3 [00:02<00:01,  1.42s/it]
Cohort.map[_DefineAndLabelWithinSubject]: 100%|██████████| 3/3 [00:04<00:00,  1.46s/it]
Cohort.map[_DefineAndLabelWithinSubject]: 100%|██████████| 3/3 [00:04<00:00,  1.46s/it]
Per-subject models: ['subj001', 'subj002', 'subj003']
subj001 [1, 2, 3]
subj002 [1, 2, 3]
subj003 [1, 2, 3]
   subject  ...  habitat_3_volume_fraction
0  subj001  ...                   0.191561
1  subj002  ...                   0.381213
2  subj003  ...                   0.352989

[3 rows x 7 columns]
subject habitat_1_voxel_count habitat_1_volume_fraction habitat_2_voxel_count habitat_2_volume_fraction habitat_3_voxel_count habitat_3_volume_fraction
0 subj001 14235.0 0.410301 13813.0 0.398138 6646.0 0.191561
1 subj002 3585.0 0.363664 2515.0 0.255123 3758.0 0.381213
2 subj003 1303.0 0.211663 2680.0 0.435348 2173.0 0.352989


Show each subject’s habitat map on its own anatomy. No comparison – just the three maps side by side.

Path("out").mkdir(exist_ok=True)
for subject, habitat_map in zip(cohort, result.habitat_maps):
    fig = plot_habitat_overlay(
        subject.image(ROI),
        habitat_map,
        title=f"habitats ({habitat_map.subject_id})",
        crop_to="labels",
    )
    fig.savefig(
        f"out/one_step_{habitat_map.subject_id}.png",
        dpi=150,
        bbox_inches="tight",
    )
    plt.show()
  • habitats (subj001), Axis 0 (axial-like) @ 96, Axis 1 (coronal-like) @ 165, Axis 2 (sagittal-like) @ 71
  • habitats (subj002), Axis 0 (axial-like) @ 86, Axis 1 (coronal-like) @ 233, Axis 2 (sagittal-like) @ 69
  • habitats (subj003), Axis 0 (axial-like) @ 46, Axis 1 (coronal-like) @ 121, Axis 2 (sagittal-like) @ 200
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(
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(
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 7.516 seconds)

Gallery generated by Sphinx-Gallery