Source code for habit.voxel_features.local_entropy

# 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.
#
"""Local-entropy voxel features: neighbourhood heterogeneity per voxel."""

from __future__ import annotations

from typing import Any, Dict, List, Optional, Sequence

import numpy as np

from habit.contracts.habitat import VoxelFeatureField
from habit.contracts.subject import Subject
from habit.voxel_features._base import (
    aligned_image,
    build_voxel_field,
    resolve_source_modalities,
    resolve_voxel_modalities,
    roi_voxels,
)
from habit.voxel_features.registry import VoxelFeatureExtractorRegistry
from habit.kernels.voxel_texture import local_entropy_map
from habit.spec.specs import Spec

__all__ = ["LocalEntropyVoxelFeatures"]


[docs] @VoxelFeatureExtractorRegistry.register("local_entropy") class LocalEntropyVoxelFeatures: """ Shannon entropy of each voxel's intensity neighbourhood. Where raw intensity says how bright a voxel is, local entropy says how disordered its surroundings are, which separates homogeneous tissue from structurally mixed tissue at the same intensity. The entropy map is computed over the whole image and then restricted to the ROI, exactly as in v0.1, so voxels at the ROI border still see their true neighbourhood. Args: modality: Single modality key -- the explicit form used inside feature trees. Mutually exclusive with ``modalities``. modalities: Modality keys to describe, in feature order; empty selects every image the subject carries. as_: Optional output-column alias. Valid only with exactly one resolved modality; the column suffix then uses the alias. roi: Mask key defining the region of interest; ``None`` uses the subject's single mask. kernel_size: Neighbourhood edge length in voxels. Even values are incremented to keep the neighbourhood centred, as in v0.1. bins: Histogram bins used to discretise intensities. """
[docs] def __init__( self, modalities: Sequence[str] = (), roi: Optional[str] = None, kernel_size: int = 3, bins: int = 32, modality: Optional[str] = None, as_: Optional[str] = None, ) -> None: if isinstance(kernel_size, bool) or not isinstance(kernel_size, int) or kernel_size < 1: raise ValueError( f"kernel_size must be a positive integer; got {kernel_size!r}." ) if isinstance(bins, bool) or not isinstance(bins, int) or bins < 2: raise ValueError(f"bins must be an integer of at least 2; got {bins!r}.") resolved, labels = resolve_source_modalities( modality, modalities, as_, owner="local_entropy" ) self.modalities = resolved self.source_labels = labels self.modality = str(modality) if modality is not None else None self.as_ = str(as_) if as_ is not None else None self.roi = roi self.kernel_size = int(kernel_size) self.bins = int(bins)
@property def spec(self) -> Spec: """Return the algorithm specification used for provenance.""" params: Dict[str, Any] = { "modalities": list(self.modalities), "roi": self.roi, "kernel_size": self.kernel_size, "bins": self.bins, } # Fold the singular/alias forms in only when set so the historical # ``modalities=[...]`` fingerprint stays byte-identical. if self.modality is not None: params["modality"] = self.modality if self.as_ is not None: params["as_"] = self.as_ return Spec(name="local_entropy", params=params)
[docs] def __call__(self, subject: Subject) -> VoxelFeatureField: """ Compute per-voxel local entropy for one subject. Args: subject: Subject providing the requested modalities and mask. Returns: One row per ROI voxel, one ``local_entropy-{source}`` column per modality, where ``source`` is the ``as_`` alias when given, else the modality name. Raises: GeometryError: If a modality and the mask are on different grids. HABITAPIError: If a requested modality is absent. """ modalities = resolve_voxel_modalities( subject, self.modalities, owner="local_entropy" ) # ``resolve_voxel_modalities`` may expand an empty request to every # subject image; labels track that expansion one-to-one. labels = ( self.source_labels if len(self.source_labels) == len(modalities) else modalities ) mask, inside, voxel_index = roi_voxels(subject, self.roi) names: List[str] = [] columns: List[np.ndarray] = [] for modality, label in zip(modalities, labels): array = aligned_image(subject, modality, mask, owner="local_entropy") entropy = local_entropy_map( array, kernel_size=self.kernel_size, bins=self.bins ) names.append(f"local_entropy-{label}") columns.append(entropy[inside]) values = np.stack(columns, axis=1) return build_voxel_field( subject, mask, voxel_index, names, values, self.spec )