Note
Go to the end to download the full example code.
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 –
fitandassignrun 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]
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()
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)


