Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
415e915
- Add extra typing with nibabelImage, VectorXd, ScalarType, etc. into…
dkuegler Dec 10, 2025
6ead7db
added corpus callosum module
ClePol Sep 16, 2025
e06844d
updated requirements and removed mri_cc from recon-surf
ClePol Sep 17, 2025
cdf7687
updated requirements, formatting, cleanup
ClePol Sep 19, 2025
d5ab726
formatting and requirements fixes
ClePol Sep 19, 2025
936b9f4
fix typo in comment
m-reuter Sep 19, 2025
6de7538
fixed spelling in comments
ClePol Sep 19, 2025
8bb8833
added checkpoint donwloading
ClePol Sep 19, 2025
f03997c
added writing soft labels
ClePol Sep 22, 2025
f06f282
cc painting script for reconsurf integration
ClePol Sep 23, 2025
fde4739
added midslice based 3D subsegmentation in orig space outputs
ClePol Sep 24, 2025
db2c70d
updated README and paths
ClePol Sep 25, 2025
26239b0
added partial volume corrected volume calculation, error messages and…
ClePol Sep 25, 2025
e097c02
sphinx doc build and license
ClePol Sep 25, 2025
a9d2963
fix typos
m-reuter Sep 26, 2025
fd6e885
added doc files for sphinx
ClePol Sep 26, 2025
d2084c2
bugfixes for hires images, README updates, error handling
ClePol Sep 29, 2025
64f5ec6
improved contour extraction for thin CC and surface coordinates
ClePol Sep 29, 2025
5e69704
fixed freesurfer surface conversion and scaling issues
ClePol Oct 1, 2025
763409f
docstrings, typehints and small bugfixes
ClePol Oct 2, 2025
825a9a8
cleaned up stats writing
ClePol Oct 2, 2025
4068e49
added consolidation strategy with WM and ventricle labels
ClePol Nov 12, 2025
1b8ecc9
FastSurfer style weights loading + removed superflous plot
ClePol Nov 12, 2025
257068d
recon-surf integration with FastSurferCNN label consolidation
ClePol Nov 12, 2025
9a9ebac
updated commandline interface
ClePol Nov 12, 2025
6024d8a
Various documentation and formatting changes as well as optimizations…
dkuegler Nov 12, 2025
908daca
Fixes broken by history rewrite (merge => rebase)
dkuegler Nov 12, 2025
3dbf31d
Fixing problems introduced by incomplete changes in review
dkuegler Nov 14, 2025
f2aa773
rename files and standardize file names
dkuegler Nov 25, 2025
f4652ff
Fix doc build errors and ruff optimization codes
dkuegler Nov 25, 2025
ebee7ad
updated helptexts & formatting
ClePol Nov 25, 2025
189cfc8
documentation and review comments
ClePol Nov 26, 2025
4d2f6df
updated logging in paint_cc_into_pred and added missing docfiles
ClePol Nov 26, 2025
6fb08c5
Improve the left_right masking.
dkuegler Nov 26, 2025
d2bee05
Fix the CorpusCallosum documentation
dkuegler Nov 27, 2025
97a7c8a
Remove --qc_output_dir and related functionality to simplify the fast…
dkuegler Nov 28, 2025
576508d
file renaming, removed unused code, documentation update
ClePol Nov 28, 2025
22768a1
fixed commandline texts, parameter and absolute paths
ClePol Dec 4, 2025
4ec8ec5
Rewrite of CCIndex, fixed middle slice selection argument, helptext
ClePol Dec 4, 2025
246fc77
Fix AC-PC localization
dkuegler Dec 4, 2025
0e0f5cf
Rework the generation of transformation matrices and make them more u…
dkuegler Dec 3, 2025
7137ba8
Fix docstrings and formatting in mesh.py
dkuegler Dec 8, 2025
3780abe
Fix ruff errors
dkuegler Dec 8, 2025
5024d0d
updated helptext
ClePol Dec 9, 2025
a4c49c0
split cc_mesh class into cc_mesh and cc_contour
ClePol Dec 9, 2025
8bab138
updated cc visualization script with cleaner interface to Mesh, Conto…
ClePol Dec 9, 2025
4f8a5a5
cleaned up visualization script logic, removed unused CC contour code…
ClePol Dec 10, 2025
2c924c8
Add the types file to merge all recurring types of FastSurferCC
dkuegler Dec 10, 2025
005917c
- Fix ruff and documentation errors
dkuegler Dec 10, 2025
de1bedf
Rename create_CC_mesh_from_contours to CCMesh.from_contours and make …
dkuegler Dec 12, 2025
bbb66c7
Fix surface output
dkuegler Dec 16, 2025
adfd523
- Clean up docstrings, typing
dkuegler Dec 19, 2025
f3dcdca
fixed label consolidation
ClePol Dec 22, 2025
8757466
visualization & commandline interface bugfixes
ClePol Dec 22, 2025
aa427e7
- Fix saving the cc_surf (only saved when thickness_image was requested
dkuegler Dec 22, 2025
d850687
Fix and add explanation on how change cc_visualization.py
dkuegler Dec 22, 2025
90ab5d7
Fix doc build error.
dkuegler Dec 22, 2025
cd7c6e9
Update CorpusCallosum to use the new lapy.Polygon interface.
dkuegler Dec 22, 2025
eb411a5
fixed for subsegmentation and lapy+cc contour
ClePol Dec 23, 2025
b3b0de9
added advanced curvature metrics and refactored curvature calculation
ClePol Dec 23, 2025
a222374
bump lapy to 1.5.0 (instead of install from github)
dkuegler Dec 24, 2025
b536b2f
adressed fixmes, fixed bugs, added warnings
ClePol Jan 5, 2026
1bdb040
adressed review comments
ClePol Jan 6, 2026
3958a97
Fix the types of CCMeasuresDict.thickness_profile and ContourList
dkuegler Jan 5, 2026
3b930b5
Fix endpoint extraction and rotation of offsets (for endpoint extract…
dkuegler Jan 6, 2026
3035c7e
Fix doc and style
dkuegler Jan 6, 2026
16dbb8f
fixed stats (subseg-area, ordering, etc.), improved visualization
ClePol Jan 6, 2026
f99adfd
fix 3d visualization
ClePol Jan 6, 2026
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
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",
]
254 changes: 254 additions & 0 deletions CorpusCallosum/cc_visualization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
import argparse
import sys
from pathlib import Path
from typing import Literal

import numpy as np

from CorpusCallosum.data.fsaverage_cc_template import load_fsaverage_cc_template
from CorpusCallosum.shape.contour import CCContour
from CorpusCallosum.shape.mesh import CCMesh
from FastSurferCNN.utils.logging import get_logger, setup_logging

logger = get_logger(__name__)


def make_parser() -> argparse.ArgumentParser:
"""Create a command line parser for the visualization pipeline."""
parser = argparse.ArgumentParser(description="Visualize corpus callosum from template files.")
parser.add_argument(
"--template_dir",
type=str,
required=True,
help=(
"Path to a template directory containing per-slice files named "
"thickness_values_<idx>.txt, and optionally contour_<idx>.txt "
"and thickness_measurement_points_<idx>.txt. If contour_<idx>.txt "
"and thickness_measurement_points_<idx>.txt are not provided, "
"uses fsaverage template."
),
metavar="TEMPLATE_DIR",
default=None,
)
parser.add_argument("--output_dir",
type=str,
required=True,
help="Directory for output files. Writes: "
"cc_mesh.html - Interactive 3D mesh visualization (HTML file) "
"midslice_2d.png - 2D midslice visualization of the corpus callosum "
"cc_mesh.vtk - VTK mesh file format "
"cc_mesh.fssurf - FreeSurfer surface file "
"cc_mesh_overlay.curv - FreeSurfer curvature overlay file "
"cc_mesh_snap.png - Screenshot/snapshot of the 3D mesh (requires whippersnappy>=1.3.1)",
metavar="OUTPUT_DIR"
)
parser.add_argument(
"--resolution",
type=float,
default=1.0,
help="Resolution in mm for the mesh.",
metavar="RESOLUTION"
)
parser.add_argument(
"--smoothing_window",
type=int,
default=5,
help="Window size for smoothing the contour.",
metavar="SMOOTHING_WINDOW"
)
parser.add_argument(
"--colormap",
type=str,
default="red_to_yellow",
choices=["red_to_blue", "blue_to_red", "red_to_yellow", "yellow_to_red"],
help="Colormap to use for thickness visualization, lower to higher values.",
)
parser.add_argument(
"--color_range",
type=float,
nargs=2,
default=None,
metavar=("MIN", "MAX"),
required=False,
help="Specify the range for the colorbar (2 values: min max). Defaults to automatic choice. \
(e.g. --color_range 0 10).",
)
parser.add_argument(
"--legend",
type=str,
default="Thickness (mm)",
help="Legend for the colorbar.",
metavar="LEGEND")
parser.add_argument(
"--twoD",
action="store_true",
help="Generate 2D visualization instead of 3D mesh.",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
default=0,
help="Enable verbose (pass twice for debug-output).",
)
return parser


def options_parse() -> argparse.Namespace:
"""Parse command line arguments for the pipeline."""
parser = make_parser()
args = parser.parse_args()

# Create output directory if it doesn't exist
Path(args.output_dir).mkdir(parents=True, exist_ok=True)
return args


def load_contours_from_template_dir(
template_dir: Path, resolution: float, smoothing_window: int
) -> list[CCContour]:
"""Load all contours and thickness data from a template directory."""
thickness_files = sorted(template_dir.glob("thickness_values_*.txt"))
if not thickness_files:
raise FileNotFoundError(
f"No thickness files found in template directory {template_dir}. "
"Expected files named thickness_values_<idx>.txt and "
"optionally contour_<idx>.txt and thickness_measurement_points_<idx>.txt."
)

fsaverage_contour = None
contours: list[CCContour] = []
# First pass: collect all indices to determine the range
indices = []
for thickness_file in thickness_files:
try:
idx = int(thickness_file.stem.split("_")[-1])
indices.append(idx)
except ValueError:
# skip files that do not follow the expected naming
continue

# Calculate z_positions centered around the middle slice
num_slices = len(indices)
middle_idx = num_slices // 2

for thickness_file in thickness_files:
try:
idx = int(thickness_file.stem.split("_")[-1])
except ValueError:
# skip files that do not follow the expected naming
continue

# Calculate z_position: use the index offset from middle, scaled by resolution
z_position = (idx - indices[middle_idx]) * resolution

contour_file = template_dir / f"contour_{idx}.txt"

if not contour_file.exists():
# get length of thickness values
thickness_values = np.loadtxt(thickness_file, dtype=str)
# get the non nan thickness values (excluding header), so we know how many points to sample
num_thickness_values = np.sum(~np.isnan(np.array(thickness_values[1:],dtype=float)))
if fsaverage_contour is None:
fsaverage_contour = load_fsaverage_cc_template()
# create measurement points (points = 2 x levelpaths) according to number of thickness values
fsaverage_contour.create_levelpaths(num_points=num_thickness_values // 2, inplace=True)
current_contour = fsaverage_contour.copy()
current_contour.z_position = z_position
current_contour.load_thickness_values(thickness_file)

else:
current_contour = CCContour.from_contour_file(contour_file, thickness_file, z_position=z_position)

if smoothing_window > 0:
current_contour.smooth_contour(window_size=smoothing_window)

current_contour.fill_thickness_values()
contours.append(current_contour)

if not contours:
raise ValueError(f"No valid contours could be loaded from {template_dir}")
return contours


def main(
template_dir: str | Path,
output_dir: str | Path,
resolution: float = 1.0,
smoothing_window: int = 5,
colormap: str = "red_to_yellow",
color_range: tuple[float, float] | None = None,
legend: str | None = None,
twoD: bool = False,
) -> Literal[0] | str:
"""Visualize corpus callosum templates in 2D or 3D."""
output_dir = Path(output_dir)
color_range = tuple(color_range) if color_range is not None else None

contours = load_contours_from_template_dir(
Path(template_dir), resolution=resolution, smoothing_window=smoothing_window,
)

# 2D visualization
mid_contour = contours[len(contours) // 2]

# for now, we only support thickness visualization, this is preparing to plot also p-values and icc values
mode = "thickness"
logger.info(f"Writing output to {output_dir / 'cc_thickness_2d.png'}")

if mode == "thickness":
raw_thickness_values = mid_contour.thickness_values[~np.isnan(mid_contour.thickness_values)]
# values are duplicated because they have two measurement points per levelpath
raw_thickness_values = raw_thickness_values[len(raw_thickness_values) // 2:]
mid_contour.plot_contour_colorfill(
plot_values=raw_thickness_values,
title=None,
save_path=str(output_dir / "cc_thickness_2d.png"),
colorbar=True,
mode=mode
)
if twoD:
return 0

# 3D visualization
cc_mesh = CCMesh.from_contours(contours, smooth=0)

plot_kwargs = dict(
colormap=colormap,
color_range=color_range,
thickness_overlay=True,
legend=legend or "",
)
cc_mesh.plot_mesh(**plot_kwargs)
cc_mesh.plot_mesh(output_path=str(output_dir / "cc_mesh.html"), **plot_kwargs)

logger.info(f"Writing vtk file to {output_dir / 'cc_mesh.vtk'}")
cc_mesh.write_vtk(str(output_dir / "cc_mesh.vtk"))
logger.info(f"Writing freesurfer surface file to {output_dir / 'cc_mesh.fssurf'}")
cc_mesh.write_fssurf(str(output_dir / "cc_mesh.fssurf"))
logger.info(f"Writing freesurfer overlay file to {output_dir / 'cc_mesh_overlay.curv'}")
cc_mesh.write_morph_data(str(output_dir / "cc_mesh_overlay.curv"))
try:
cc_mesh.snap_cc_picture(str(output_dir / "cc_mesh_snap.png"))
logger.info(f"Writing 3D snapshot image to {output_dir / 'cc_mesh_snap.png'}")
except RuntimeError:
logger.warning("The cc_visualization script requires whippersnappy>=1.3.1 to makes screenshots, install with "
"`pip install whippersnappy>=1.3.1` !")
return 0

if __name__ == "__main__":
options = options_parse()

# Set up logging if verbose mode is enabled
setup_logging(None, options.verbose) # Log to stdout only

sys.exit(main(
template_dir=options.template_dir,
output_dir=options.output_dir,
resolution=options.resolution,
smoothing_window=options.smoothing_window,
colormap=options.colormap,
color_range=options.color_range,
legend=options.legend,
twoD=options.twoD,
))
7 changes: 7 additions & 0 deletions CorpusCallosum/config/checkpoint_paths.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
url:
- "https://zenodo.org/records/17141933/files"
- "https://b2share.fz-juelich.de/api/files/e4eb699c-ba68-4470-9f3d-89ceeee1a334"

checkpoint:
segmentation: "checkpoints/FastSurferCC_segmentation_v1.0.0.pkl"
localization: "checkpoints/FastSurferCC_localization_v1.0.0.pkl"
Empty file added CorpusCallosum/data/__init__.py
Empty file.
57 changes: 57 additions & 0 deletions CorpusCallosum/data/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# 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.

from FastSurferCNN.utils.parser_defaults import FASTSURFER_ROOT

### Constants
WEIGHTS_PATH = FASTSURFER_ROOT / "checkpoints"
FSAVERAGE_CENTROIDS_PATH = FASTSURFER_ROOT / "CorpusCallosum" / "data" / "fsaverage_centroids.json"
# Contains both affine and header
FSAVERAGE_DATA_PATH = FASTSURFER_ROOT / "CorpusCallosum" / "data" / "fsaverage_data.json"
FSAVERAGE_MIDDLE = 128 # Middle slice index in fsaverage space
CC_LABEL = 192 # Label value for corpus callosum in segmentation
FORNIX_LABEL = 250 # Label value for fornix in segmentation
THIRD_VENTRICLE_LABEL = 4 # Label value for third ventricle in segmentation
SUBSEGMENT_LABELS = [251, 252, 253, 254, 255] # labels for subsegments in segmentation


DEFAULT_INPUT_PATHS = {
"conf_name": "mri/orig.mgz",
"aseg_name": "mri/aparc.DKTatlas+aseg.deep.mgz",
}

DEFAULT_OUTPUT_PATHS = {
## images
"upright_volume": None, # orig.mgz mapped to upright space
## segmentations
"segmentation": "mri/callosum.CC.upright.mgz", # corpus callosum segmentation in upright space
"segmentation_in_orig": "mri/callosum.CC.orig.mgz", # cc segmentation in input segmentations space
"softlabels_cc": "mri/callosum.CC.soft.mgz", # cc softlabels in upright space
"softlabels_fn": "mri/fornix.CC.soft.mgz", # fornix softlabels in upright space
"softlabels_background": "mri/background.CC.soft.mgz", # background softlabels in upright space
## stats
"cc_markers": "stats/callosum.CC.midslice.json", # cc metrics for middle slice
"cc_measures": "stats/callosum.CC.all_slices.json", # cc metrics for all slices
## transforms
"upright_lta": "mri/transforms/cc_up.lta", # lta transform from orig to upright space
"orient_volume_lta": "mri/transforms/orient_volume.lta", # lta transform from orig to upright+acpc corrected space
## qc
"qc_image": None, #"callosum.png", # debug image of cc contours
"thickness_image": None, # "callosum.thickness.png", # whippersnappy 3D image of cc thickness
"cc_html": None, # "corpus_callosum.html", # plotly cc visualization
## surface
"cc_surf": "surf/callosum.surf", # cc surface file
"cc_thickness_overlay": "surf/callosum.thickness.w", # cc surface overlay file
"cc_surf_vtk": "surf/callosum.vtk", # vtk file of cc mesh
}
Loading