Skip to content

tissue_analyzer

tit.pre.tissue_analyzer

Tissue analysis for TI-Toolbox.

Analyzes tissue types (CSF, bone, skin) from segmented NIfTI data, calculating volumes, thickness, and generating visualizations (thickness maps and methodology figures).

Public API

run_tissue_analysis Run tissue analysis for one or more tissue types for a subject. TissueAnalyzer Low-level class that performs volume, thickness, and visualization analysis on a single tissue type.

See Also

tit.pre.structural.run_pipeline : Full preprocessing pipeline. tit.pre.charm : CHARM head-mesh generation (produces the segmentation input consumed here).

TissueAnalyzer

TissueAnalyzer(nifti_path: str | Path, output_dir: str | Path, tissue_type: str, logger: Logger)

Analyze a single tissue type from segmented NIfTI data.

Loads a labeling NIfTI volume, extracts the requested tissue mask, filters it to the brain region, computes thickness statistics via a 3-D distance transform, and writes a text report plus optional matplotlib visualizations.

Parameters

nifti_path : str or pathlib.Path Path to the segmented labeling NIfTI file (e.g. labeling.nii.gz). output_dir : str or pathlib.Path Directory where reports and figures are written. tissue_type : str One of 'csf', 'bone', or 'skin'. logger : logging.Logger Logger for progress messages.

Attributes

tissue_name : str Human-readable tissue name (e.g. 'CSF'). tissue_labels : list[int] Segmentation label IDs for this tissue. voxel_volume : float Volume of a single voxel in mm^3.

Raises

PreprocessError If tissue_type is not a recognised key in TISSUE_CONFIGS.

See Also

run_tissue_analysis : Convenience wrapper that analyses multiple tissues.

Source code in tit/pre/tissue_analyzer.py
def __init__(
    self,
    nifti_path: str | Path,
    output_dir: str | Path,
    tissue_type: str,
    logger: logging.Logger,
):
    self.nifti_path = Path(nifti_path)
    self.output_dir = Path(output_dir)
    self.output_dir.mkdir(parents=True, exist_ok=True)
    self.logger = logger

    if tissue_type not in TISSUE_CONFIGS:
        raise PreprocessError(f"Unknown tissue type: {tissue_type}")

    config = TISSUE_CONFIGS[tissue_type]
    self.tissue_name = config["name"]
    self.tissue_labels = config["labels"]
    self.padding = config["padding"]
    self.color_scheme = config["color_scheme"]
    self.tissue_color = config["tissue_color"]
    self.brain_labels = config["brain_labels"]

    # Load NIfTI
    self.logger.info(f"Loading NIfTI: {nifti_path}")
    self.nii = nib.load(str(nifti_path))
    self.data = self.nii.get_fdata()
    self.voxel_dims = self.nii.header.get_zooms()[:3]
    self.voxel_volume = float(np.prod(self.voxel_dims))

    # Load label names
    self.label_names = self._load_label_names()

analyze

analyze() -> dict

Run the complete tissue analysis.

Returns

dict Analysis results with keys 'volume_cm3', 'volume_mm3', 'thickness' (dict with mean/std/min/max), 'voxels' (dict with total/filtered), and 'report_path'.

Source code in tit/pre/tissue_analyzer.py
def analyze(self) -> dict:
    """Run the complete tissue analysis.

    Returns
    -------
    dict
        Analysis results with keys ``'volume_cm3'``, ``'volume_mm3'``,
        ``'thickness'`` (dict with mean/std/min/max), ``'voxels'``
        (dict with total/filtered), and ``'report_path'``.
    """
    self.logger.info(f"Analyzing {self.tissue_name}...")

    # Create masks
    tissue_mask = self._create_tissue_mask()
    brain_mask = self._create_brain_mask()

    total_voxels = int(np.sum(tissue_mask))
    if total_voxels == 0:
        self.logger.warning(f"No {self.tissue_name} tissue found")
        return {
            "volume_cm3": 0,
            "thickness": {"mean": 0, "std": 0, "min": 0, "max": 0},
        }

    # Filter to brain region
    if np.sum(brain_mask) > 0:
        filtered_mask = self._filter_to_brain_region(tissue_mask, brain_mask)
    else:
        filtered_mask = tissue_mask

    filtered_voxels = int(np.sum(filtered_mask))
    self.logger.debug(
        f"Voxels: {total_voxels:,} total, {filtered_voxels:,} filtered"
    )

    # Calculate thickness
    thickness_stats = self._calculate_thickness(filtered_mask)

    # Calculate volume
    volume_mm3 = filtered_voxels * self.voxel_volume
    volume_cm3 = volume_mm3 / 1000

    # Generate outputs
    self._create_visualizations(
        tissue_mask, brain_mask, filtered_mask, thickness_stats
    )
    report_path = self._write_report(tissue_mask, filtered_mask, thickness_stats)

    self.logger.info(
        f"{self.tissue_name}: Volume={volume_cm3:.2f}cm³, "
        f"Thickness={thickness_stats['mean']:.2f}±{thickness_stats['std']:.2f}mm"
    )

    return {
        "volume_cm3": volume_cm3,
        "volume_mm3": volume_mm3,
        "thickness": {
            "mean": thickness_stats["mean"],
            "std": thickness_stats["std"],
            "min": thickness_stats["min"],
            "max": thickness_stats["max"],
        },
        "voxels": {"total": total_voxels, "filtered": filtered_voxels},
        "report_path": str(report_path),
    }

run_tissue_analysis

run_tissue_analysis(project_dir: str, subject_id: str, *, tissues: Iterable[str] = DEFAULT_TISSUES, logger: Logger) -> dict

Run tissue analysis for a subject.

Creates a TissueAnalyzer for each requested tissue type, computes volume and thickness statistics, and writes reports and visualizations.

Parameters

project_dir : str BIDS project root. subject_id : str Subject identifier without the sub- prefix. tissues : iterable of str, optional Tissue types to analyze. Defaults to ('bone', 'csf', 'skin'). logger : logging.Logger Logger for progress output.

Returns

dict Mapping of tissue name to per-tissue analysis results (volume, thickness, voxel counts, report path).

Raises

PreprocessError If the segmentation labeling file does not exist.

See Also

TissueAnalyzer : Low-level analysis for a single tissue type. run_pipeline : Full preprocessing pipeline.

Source code in tit/pre/tissue_analyzer.py
def run_tissue_analysis(
    project_dir: str,
    subject_id: str,
    *,
    tissues: Iterable[str] = DEFAULT_TISSUES,
    logger: logging.Logger,
) -> dict:
    """Run tissue analysis for a subject.

    Creates a ``TissueAnalyzer`` for each requested tissue type, computes
    volume and thickness statistics, and writes reports and visualizations.

    Parameters
    ----------
    project_dir : str
        BIDS project root.
    subject_id : str
        Subject identifier without the ``sub-`` prefix.
    tissues : iterable of str, optional
        Tissue types to analyze.  Defaults to ``('bone', 'csf', 'skin')``.
    logger : logging.Logger
        Logger for progress output.

    Returns
    -------
    dict
        Mapping of tissue name to per-tissue analysis results (volume,
        thickness, voxel counts, report path).

    Raises
    ------
    PreprocessError
        If the segmentation labeling file does not exist.

    See Also
    --------
    TissueAnalyzer : Low-level analysis for a single tissue type.
    run_pipeline : Full preprocessing pipeline.
    """
    pm = get_path_manager(project_dir)

    label_path = Path(pm.tissue_labeling(subject_id))
    if not label_path.exists():
        raise PreprocessError(f"labeling.nii.gz not found: {label_path}")

    output_root = Path(pm.ensure(pm.tissue_analysis_output(subject_id)))
    results = {}

    for tissue in tissues:
        if tissue not in TISSUE_CONFIGS:
            logger.warning(f"Unknown tissue type: {tissue}, skipping")
            continue

        output_dir = output_root / f"{tissue}_analysis"
        analyzer = TissueAnalyzer(label_path, output_dir, tissue, logger)
        results[tissue] = analyzer.analyze()

    return results