Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
0fdb284
added corpus callosum module
ClePol Sep 16, 2025
0c0ab6e
updated requirements and removed mri_cc from recon-surf
ClePol Sep 17, 2025
2ac9fe7
updated requirements, formatting, cleanup
ClePol Sep 19, 2025
01c6000
formatting and requirements fixes
ClePol Sep 19, 2025
6962f0a
fix typo in comment
m-reuter Sep 19, 2025
ef6a7b5
fixed spelling in comments
ClePol Sep 19, 2025
89c4e5b
added checkpoint donwloading
ClePol Sep 19, 2025
826844c
added writing soft labels
ClePol Sep 22, 2025
374a2d7
cc painting script for reconsurf integration
ClePol Sep 23, 2025
d2061f3
added midslice based 3D subsegmentation in orig space outputs
ClePol Sep 24, 2025
baeb35e
updated README and paths
ClePol Sep 25, 2025
603ff8f
added partial volume corrected volume calculation, error messages and…
ClePol Sep 25, 2025
4af9dc2
sphinx doc build and license
ClePol Sep 25, 2025
9cac82c
fix typos
m-reuter Sep 26, 2025
b17ca18
added doc files for sphinx
ClePol Sep 26, 2025
9bfdd09
bugfixes for hires images, README updates, error handling
ClePol Sep 29, 2025
d6009ec
improved contour extraction for thin CC and surface coordinates
ClePol Sep 29, 2025
d3356d8
fixed freesurfer surface conversion and scaling issues
ClePol Oct 1, 2025
0f5e00d
docstrings, typehints and small bugfixes
ClePol Oct 2, 2025
6c69367
cleaned up stats writing
ClePol Oct 2, 2025
039d071
added consolidation strategy with WM and ventricle labels
ClePol Nov 12, 2025
08d1e18
edited variable names
ClePol Nov 12, 2025
cdc47f8
FastSurfer style weights loading + removed superflous plot
ClePol Nov 12, 2025
84c6193
recon-surf integration with FastSurferCNN label consolidation
ClePol Nov 12, 2025
d09083c
updated commandline interface
ClePol Nov 12, 2025
2f187ea
Various documentation and formatting changes as well as optimizations…
dkuegler Nov 12, 2025
2811ddf
Fixes broken by history rewrite (merge => rebase)
dkuegler Nov 12, 2025
727fc6c
Fixing problems introduced by incomplete changes in review
dkuegler Nov 14, 2025
eb43eba
rename files and standardize file names
dkuegler Nov 25, 2025
4651fb2
Fix doc build errors and ruff optimization codes
dkuegler Nov 25, 2025
726bb00
updated helptexts & formatting
ClePol Nov 25, 2025
83f2fe6
documentation and review comments
ClePol Nov 26, 2025
79e9f95
updated logging in paint_cc_into_pred and added missing docfiles
ClePol Nov 26, 2025
02186e5
Improve the left_right masking.
dkuegler Nov 26, 2025
ddb174c
Fix countours spelling error
dkuegler Nov 26, 2025
bd78d56
Fix the CorpusCallosum documentation
dkuegler Nov 27, 2025
40308b4
Remove --qc_output_dir and related functionality to simplify the fast…
dkuegler Nov 28, 2025
33628b8
file renaming, removed unused code, documentation update
ClePol Nov 28, 2025
a17b018
fixed commandline texts, parameter and absolute paths
ClePol Dec 4, 2025
77bd663
Rewrite of CCIndex, fixed middle slice selection argument, helptext
ClePol Dec 4, 2025
e7c7544
Fix AC-PC localization
dkuegler Dec 4, 2025
5c315cc
Various fixes to CC generation (messages to be edited)
dkuegler Dec 3, 2025
12506df
Fix docstrings and formatting in mesh.py
dkuegler Dec 8, 2025
51ad1a1
Fix ruff errors
dkuegler Dec 8, 2025
4ac7a2d
updated helptext
ClePol Dec 9, 2025
2bbff12
split cc_mesh class into cc_mesh and cc_contour
ClePol Dec 9, 2025
496c462
updated cc visualization script with cleaner interface to Mesh, Conto…
ClePol Dec 9, 2025
85d3675
cleaned up visualization script logic, removed unused CC contour code…
ClePol Dec 10, 2025
471e112
Lots of changes
dkuegler Dec 10, 2025
c100efc
- Fix ruff and documentation errors
dkuegler Dec 10, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 17 additions & 7 deletions CerebNet/data_loader/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,26 @@
logger = logging.get_logger(__name__)


def get_dataloader(cfg, mode):
def get_dataloader(cfg: object, mode: str) -> DataLoader:
"""
Creating the dataset and pytorch data loader
Create the dataset and pytorch data loader.

Args:
cfg:
mode: loading data for train, val and test mode
Parameters
----------
cfg : object
Configuration object containing data loading parameters.
mode : str
Loading mode - either 'train' or 'val'.

Returns:
the Dataloader
Returns
-------
DataLoader
PyTorch DataLoader configured based on the mode.

Raises
------
ValueError
If mode is not 'train' or 'val'.
"""

if mode == "train":
Expand Down
40 changes: 27 additions & 13 deletions CerebNet/datasets/load_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,23 @@ def load_test_subject(self, current_subject):

def _get_roi_extracted_data(self, img, label, talairach):
"""
Finding the bounding volume and returning extracted img and label
according to roi
Args:
img:
label:

Returns:
img and label resized according to roi and patch size
Finding the bounding volume and returning extracted img and label according to roi.

Parameters
----------
img : np.ndarray
Input image volume
label : np.ndarray
Input label volume
talairach : np.ndarray or None
Talairach coordinates array

Returns
-------
tuple[np.ndarray, np.ndarray, np.ndarray or None]
- Resized image according to ROI and patch size
- Resized label according to ROI and patch size
- Resized and normalized talairach coordinates if provided, None otherwise
"""
roi = utils.bounding_volume(label, self.patch_size)
img = utils.map_size(img[roi], self.patch_size)
Expand All @@ -171,12 +180,17 @@ def _get_roi_extracted_data(self, img, label, talairach):

def _load_auxiliary_data(self, aux_subjects_path):
"""
Loading auxiliary data create by registration of original images
Args:
subjects_path: list of full path to auxiliary data
Loading auxiliary data create by registration of original images.

Returns:
dictionary with list of warped images and labels
Parameters
----------
aux_subjects_path : list
List of full paths to auxiliary data.

Returns
-------
dict
Dictionary containing lists of warped images and labels.
"""
aux_data = {"auxiliary_img": [], "auxiliary_lbl": []}
for t1_path, lbl_path in aux_subjects_path:
Expand Down
110 changes: 8 additions & 102 deletions CerebNet/datasets/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@

# IMPORTS
from collections.abc import Sequence
from pathlib import Path
from typing import Literal, TypedDict, TypeVar
from typing import TypeVar

import nibabel as nib
import numpy as np
import torch
from numpy import typing as npt

from FastSurferCNN.data_loader.conform import getscale, scalecrop
from FastSurferCNN.utils import logging

_TShape = TypeVar("_TShape", bound=tuple[int, ...])

logger = logging.getLogger(__name__)

CLASS_NAMES = {
Expand Down Expand Up @@ -63,33 +63,6 @@
subseg_labels = {"cereb_subseg": np.array(list(CLASS_NAMES.values()))}

AT = TypeVar("AT", np.ndarray, torch.Tensor)
AffineMatrix4x4 = np.ndarray[tuple[Literal[4], Literal[4]], np.dtype[float]]


class LTADict(TypedDict):
type: int
nxforms: int
mean: list[float]
sigma: float
lta: AffineMatrix4x4
src_valid: int
src_filename: str
src_volume: list[int]
src_voxelsize: list[float]
src_xras: list[float]
src_yras: list[float]
src_zras: list[float]
src_cras: list[float]
dst_valid: int
dst_filename: str
dst_volume: list[int]
dst_voxelsize: list[float]
dst_xras: list[float]
dst_yras: list[float]
dst_zras: list[float]
dst_cras: list[float]
src: npt.NDArray[float]
dst: npt.NDArray[float]


def define_size(mov_dim, ref_dim):
Expand Down Expand Up @@ -356,78 +329,10 @@ def apply_warp_field(dform_field, img, interpol_order=3):
return deformed_img


def read_lta(file: Path | str) -> LTADict:
"""Read the LTA info."""
import re
from functools import partial

import numpy as np
parameter_pattern = re.compile("^\\s*([^=]+)\\s*=\\s*([^#]*)\\s*(#.*)")
vol_info_pattern = re.compile("^(.*) volume info$")
shape_pattern = re.compile("^(\\s*\\d+)+$")
matrix_pattern = re.compile("^(-?\\d+\\.\\S+\\s+)+$")

_Type = TypeVar("_Type", bound=type)

def _vector(_a: str, dtype: type[_Type] = float, count: int = -1) -> list[_Type]:
return np.fromstring(_a, dtype=dtype, count=count, sep=" ").tolist()

parameters = {
"type": int,
"nxforms": int,
"mean": partial(_vector, dtype=float, count=3),
"sigma": float,
"subject": str,
"fscale": float,
}
vol_info_par = {
"valid": int,
"filename": str,
"volume": partial(_vector, dtype=int, count=3),
"voxelsize": partial(_vector, dtype=float, count=3),
**{f"{c}ras": partial(_vector, dtype=float) for c in "xyzc"}
}

with open(file) as f:
lines = f.readlines()

items = []
shape_lines = []
matrix_lines = []
section = ""
for i, line in enumerate(lines):
if line.strip() == "":
continue
if hits := parameter_pattern.match(line):
name = hits.group(1)
if section and name in vol_info_par:
items.append((f"{section}_{name}", vol_info_par[name](hits.group(2))))
elif name in parameters:
section = ""
items.append((name, parameters[name](hits.group(2))))
else:
raise NotImplementedError(f"Unrecognized type string in lta-file "
f"{file}:{i+1}: '{name}'")
elif hits := vol_info_pattern.match(line):
section = hits.group(1)
# not a parameter line
elif shape_pattern.search(line):
shape_lines.append(np.fromstring(line, dtype=int, count=-1, sep=" "))
elif matrix_pattern.search(line):
matrix_lines.append(np.fromstring(line, dtype=float, count=-1, sep=" "))

shape_lines = list(map(tuple, shape_lines))
lta = dict(items)
if lta["nxforms"] != len(shape_lines):
raise OSError("Inconsistent lta format: nxforms inconsistent with shapes.")
if len(shape_lines) > 1 and np.any(np.not_equal([shape_lines[0]], shape_lines[1:])):
raise OSError(f"Inconsistent lta format: shapes inconsistent {shape_lines}")
lta_matrix = np.asarray(matrix_lines).reshape((-1,) + shape_lines[0].shape)
lta["lta"] = lta_matrix
return lta


def load_talairach_coordinates(tala_path, img_shape, vox2ras):
"""Load talairach coordinates from file."""
from FastSurferCNN.utils.lta import read_lta

tala_lta = read_lta(tala_path)
# create image grid p
x, y, z = np.meshgrid(
Expand All @@ -448,7 +353,8 @@ def load_talairach_coordinates(tala_path, img_shape, vox2ras):
return tala_coordinates


def normalize_array(arr):
def normalize_array(arr: np.ndarray[_TShape, np.number]) -> np.ndarray[_TShape, np.floating]:
"""Normalize the data array to [0, 1]."""
min = arr.min()
max = arr.max()

Expand Down
12 changes: 8 additions & 4 deletions CerebNet/inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,16 @@ def _convert(plane: Plane) -> torch.Tensor:
def _view_aggregation(self, logits: dict[Plane, torch.Tensor]) -> torch.Tensor:
"""
Aggregate the view (axial, coronal, sagittal) into one volume and get the
class of the largest probability. (argmax)
class of the largest probability (argmax).

Args:
logits: dictionary of per plane predicted logits (axial, coronal, sagittal)
Parameters
----------
logits : dict[Plane, torch.Tensor]
Dictionary of per plane predicted logits (axial, coronal, sagittal)

Returns:
Returns
-------
torch.Tensor
Tensor of classes (of largest aggregated logits)
"""
aggregated_logits = torch.add(
Expand Down
43 changes: 29 additions & 14 deletions CerebNet/utils/lr_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,20 @@ def __init__(self, optimizer, *args, T_0=10, Tmult=1, lr_restart=None, **kwargs)
and a number, it is reset to initial lr * (lr_restart) ^ i, if lr_restart is a function,
the lr gets reset to lr_restart(initial_lr, i).

Args:
...: same as ReduceLROnPlateau
T_0 (optional): number of epochs until first restart (default: 10)
Tmult (optional): multiplicative factor for future restarts (default: 1)
lr_restart (optinoal): multiplicative factor for learning rate adjustment at restart.
Parameters
----------
optimizer : torch.optim.Optimizer
Wrapped optimizer
*args
Arguments passed to ReduceLROnPlateau
T_0 : int, optional
Number of epochs until first restart. Default is 10
Tmult : int, optional
Multiplicative factor for future restarts. Default is 1
lr_restart : float or callable, optional
Multiplicative factor for learning rate adjustment at restart
**kwargs
Keyword arguments passed to ReduceLROnPlateau
"""
# from torch.optim.lr_scheduler._LRSchduler
# if last_epoch == -1:
Expand Down Expand Up @@ -175,15 +184,21 @@ def _get_warmup_factor_at_iter(
Return the learning rate warmup factor at a specific iteration.
See :paper:`in1k1h` for more details.

Args:
method (str): warmup method; either "constant" or "linear".
iter (int): iteration at which to calculate the warmup factor.
warmup_iters (int): the number of warmup iterations.
warmup_factor (float): the base warmup factor (the meaning changes according
to the method used).

Returns:
float: the effective warmup factor at the given iteration.
Parameters
----------
method : str
Warmup method; either "constant" or "linear"
iter : int
Iteration at which to calculate the warmup factor
warmup_iters : int
The number of warmup iterations
warmup_factor : float
The base warmup factor (the meaning changes according to the method used)

Returns
-------
float
The effective warmup factor at the given iteration
"""
if iter >= warmup_iters:
return 1.0
Expand Down
16 changes: 16 additions & 0 deletions CorpusCallosum/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Corpus Callosum Pipeline

A deep learning-based pipeline for automated segmentation, analysis, and shape analysis of the corpus callosum in brain MRI scans.
Also segments the fornix, localizes the anterior and posterior commissure (AC and PC) and standardizes the orientation of the brain.

For detailed documentation, please refer to:
- [Module Overview](../doc/overview/modules/CC.md): Detailed description of the pipeline, workflow, and analysis options.
- [Output Files](../doc/overview/OUTPUT_FILES.md#corpus-callosum-module): List of output files and their descriptions.

## Quickstart

```bash
python3 fastsurfer_cc.py --sd /path/to/fastsurfer/output --sid test-case --verbose
```

Gives all standard outputs. The corpus callosum morphometry can be found at `stats/callosum.CC.midslice.json` including 100 thickness measurements and the areas of sub-segments.
20 changes: 20 additions & 0 deletions CorpusCallosum/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2025 AI in Medical Imaging, German Center for Neurodegenerative Diseases(DZNE), Bonn
#
# 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.

__all__ = [
"data",
"segmentation",
"transforms",
"utils",
]
Loading
Loading