Source code for habit.habitat_features.ith

# Copyright (c) 2024-2026 Li Chao, Dong Mengshi and HABIT Contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
"""ITH score (topological fragmentation) habitat features."""

from __future__ import annotations

from typing import Dict

import numpy as np

from habit.contracts.habitat import HabitatMap
from habit.contracts.subject import Subject
from habit.contracts.table import FeatureTable
from habit.habitat_features._base import single_subject_table
from habit.habitat_features.registry import HabitatFeatureExtractorRegistry
from habit.kernels.habitat_metrics import habitat_region_stats, ith_score
from habit.spec.specs import Spec

__all__ = ["IthHabitatFeatures"]


[docs] @HabitatFeatureExtractorRegistry.register("ith_score") class IthHabitatFeatures: """ ITH score for one subject's habitat map. The score quantifies how fragmented the habitat partition is: ``1 - (1 / S_total) * sum_i(S_i,max / n_i)`` over connected components of each habitat. It is numerically identical to the v0.1 ``ITHFeatureExtractor``. By default the table has a single column, ``ith_score``. Pass ``include_auxiliary=True`` (or ``Spec("ith_score", {"include_auxiliary": True})``) to also write the prefixed summaries ``ith_num_habitats`` / ``ith_total_area`` and the per-habitat ``habitat_{id}_regions`` / ``_largest_area`` / ``_area_ratio`` columns. Those extras are emitted for every id the model can assign (zeros when the habitat is absent) so cohort tables stay rectangular. The prefixes avoid colliding with ``non_radiomics`` ``num_habitats``. """
[docs] def __init__(self, include_auxiliary: bool = False) -> None: self.include_auxiliary = bool(include_auxiliary)
@property def spec(self) -> Spec: """Return the algorithm specification.""" return Spec( name="ith_score", params={"include_auxiliary": self.include_auxiliary}, )
[docs] def __call__(self, subject: Subject, habitat_map: HabitatMap) -> FeatureTable: """ Compute the ITH feature family for one subject. Args: subject: Owning subject (labels suffice; intensities unused). habitat_map: Habitat labels for that subject. Returns: One-row table. Default is ``ith_score`` only. """ labels = np.asarray(habitat_map.label_array) if not self.include_auxiliary: features: Dict[str, float] = {"ith_score": ith_score(labels)} return single_subject_table( subject_id=subject.subject_id, features=features, habitat_map=habitat_map, spec=self.spec, ) stats = habitat_region_stats(labels) features = { "ith_score": ith_score(labels, region_stats=stats), "ith_num_habitats": float(len(stats)), "ith_total_area": float(np.count_nonzero(labels)), } for habitat_id in habitat_map.habitat_ids: num_regions, largest = stats.get(int(habitat_id), (0, 0)) features[f"habitat_{habitat_id}_regions"] = float(num_regions) features[f"habitat_{habitat_id}_largest_area"] = float(largest) features[f"habitat_{habitat_id}_area_ratio"] = ( float(largest) / num_regions if num_regions > 0 else 0.0 ) return single_subject_table( subject_id=subject.subject_id, features=features, habitat_map=habitat_map, spec=self.spec, )