Source code for habit.supervoxel.slic

# 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.
#
"""SLIC supervoxels: the reference Supervoxelizer."""

from __future__ import annotations

from typing import Any, Dict, Mapping, Optional

import numpy as np

from habit.exceptions import HABITAPIError
from habit.contracts.habitat import Supervoxelization, VoxelFeatureField
from habit.supervoxel.registry import SupervoxelizerRegistry
from habit.supervoxel.features_base import aggregate_voxel_means
from habit.spec.specs import Spec
from habit.utils.estimator_utils import (
    check_passthrough_accepted,
    validate_estimator_params,
)

__all__ = ["SlicSupervoxelizer"]


[docs] @SupervoxelizerRegistry.register("slic") class SlicSupervoxelizer: """ Partition the ROI into SLIC supervoxels and average features within each. SLIC (Simple Linear Iterative Clustering) groups spatially coherent, feature-similar voxels; the v0.1 pipeline exposed the same algorithm through its clustering factory. Here it is one ordinary subject-level operator: field in, partition out. Implements :class:`~habit._protocols.Seedable` so every supervoxelizer shares the same seeding surface as kmeans/gmm and so ``HabitatSpec.random_seed`` reaches this stage during assembly. The current ``skimage.segmentation.slic`` backend has no RNG parameter; ``set_random_state`` therefore records the seed for API uniformity and future backends without changing today's deterministic partitions. Args: n_supervoxels: Requested number of supervoxels. Clamped to the number of ROI voxels (a partition cannot have more non-empty regions than voxels). compactness: Balance between colour similarity and spatial proximity (``skimage.segmentation.slic`` semantics). enforce_connectivity: When ``True``, disconnected segments are relabelled so every supervoxel is connected. estimator_params: Extra keyword arguments forwarded verbatim to ``skimage.segmentation.slic`` (e.g. ``{"sigma": 1.0}``), for vendor parameters HABIT does not declare. Keys colliding with a declared parameter or with a call argument HABIT controls (``n_segments``, ``mask``, ``channel_axis``, ``start_label``) are rejected, and every key is validated against the vendor signature at call time: a key recorded in the spec fingerprint must reach the vendor function, never be silently dropped. """
[docs] def __init__( self, n_supervoxels: int = 100, compactness: float = 10.0, enforce_connectivity: bool = True, estimator_params: Optional[Mapping[str, Any]] = None, ) -> None: if isinstance(n_supervoxels, bool) or not isinstance(n_supervoxels, int) or n_supervoxels < 1: raise HABITAPIError( f"n_supervoxels must be positive; got {n_supervoxels}." ) if isinstance(compactness, bool) or not isinstance(compactness, (int, float)) or compactness <= 0: raise HABITAPIError(f"compactness must be positive; got {compactness!r}.") self.n_supervoxels = int(n_supervoxels) self.compactness = float(compactness) self.enforce_connectivity = bool(enforce_connectivity) self.estimator_params: Dict[str, Any] = validate_estimator_params( estimator_params, declared=("n_supervoxels", "compactness", "enforce_connectivity"), fixed=("n_segments", "mask", "channel_axis", "start_label"), owner="supervoxelizer.slic", ) # Default matches other Seedable supervoxelizers (fixed seed 0). self._seed = 0
@property def spec(self) -> Spec: """Return the algorithm specification.""" params: Dict[str, Any] = { "n_supervoxels": self.n_supervoxels, "compactness": self.compactness, "enforce_connectivity": self.enforce_connectivity, } # Fold the passthrough in only when non-empty so the default # configuration keeps its historical fingerprint. if self.estimator_params: params["estimator_params"] = dict(self.estimator_params) return Spec(name="slic", params=params)
[docs] def set_random_state(self, seed: int) -> None: """ Record the study seed for this supervoxelizer. Args: seed: Non-negative study seed from ``HabitatSpec.random_seed`` or an explicit caller. Stored for Seedable uniformity; the current skimage SLIC call does not consume it. """ self._seed = int(seed)
[docs] def __call__(self, field: VoxelFeatureField) -> Supervoxelization: """ Group voxels into supervoxels and aggregate their features. Args: field: Per-voxel features for one subject. Returns: The supervoxel partition (``0`` = outside ROI, ``1..K`` = supervoxels) together with per-supervoxel mean features. Pass a :class:`~habit._protocols.SupervoxelFeatureExtractor` to the pipeline to describe the same regions differently. """ # scikit-image is required. Import here rather than at module # scope so a broken skimage install surfaces as # OptionalDependencyError instead of breaking registry import. from habit.utils.optional_deps import require slic = require( "skimage.segmentation", extra="slic", purpose="SLIC supervoxel segmentation (supervoxelizer 'slic')", ).slic check_passthrough_accepted( slic, self.estimator_params, owner="supervoxelizer.slic" ) shape = tuple(int(v) for v in field.geometry.shape) n_voxels, n_features = field.values.shape dense = np.zeros((*shape, n_features), dtype=np.float64) dense[tuple(field.voxel_index.T)] = field.values mask = np.zeros(shape, dtype=bool) mask[tuple(field.voxel_index.T)] = True n_segments = max(1, min(self.n_supervoxels, n_voxels)) labels = slic( dense, n_segments=n_segments, compactness=self.compactness, mask=mask, channel_axis=-1, start_label=1, enforce_connectivity=self.enforce_connectivity, **self.estimator_params, ) labels = np.where(mask, labels, 0).astype(np.int32) features = aggregate_voxel_means(field, labels) provenance = field.provenance.derive( produced_by=f"supervoxelizer.{self.spec.name}", spec_fingerprint=self.spec.fingerprint(), ) return Supervoxelization( subject_id=field.subject_id, label_array=labels, features=features, geometry=field.geometry, provenance=provenance, )