-
Notifications
You must be signed in to change notification settings - Fork 144
Add FastSurfer-CC to the FastSurfer repository #727
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
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 6ead7db
added corpus callosum module
ClePol e06844d
updated requirements and removed mri_cc from recon-surf
ClePol cdf7687
updated requirements, formatting, cleanup
ClePol d5ab726
formatting and requirements fixes
ClePol 936b9f4
fix typo in comment
m-reuter 6de7538
fixed spelling in comments
ClePol 8bb8833
added checkpoint donwloading
ClePol f03997c
added writing soft labels
ClePol f06f282
cc painting script for reconsurf integration
ClePol fde4739
added midslice based 3D subsegmentation in orig space outputs
ClePol db2c70d
updated README and paths
ClePol 26239b0
added partial volume corrected volume calculation, error messages and…
ClePol e097c02
sphinx doc build and license
ClePol a9d2963
fix typos
m-reuter fd6e885
added doc files for sphinx
ClePol d2084c2
bugfixes for hires images, README updates, error handling
ClePol 64f5ec6
improved contour extraction for thin CC and surface coordinates
ClePol 5e69704
fixed freesurfer surface conversion and scaling issues
ClePol 763409f
docstrings, typehints and small bugfixes
ClePol 825a9a8
cleaned up stats writing
ClePol 4068e49
added consolidation strategy with WM and ventricle labels
ClePol 1b8ecc9
FastSurfer style weights loading + removed superflous plot
ClePol 257068d
recon-surf integration with FastSurferCNN label consolidation
ClePol 9a9ebac
updated commandline interface
ClePol 6024d8a
Various documentation and formatting changes as well as optimizations…
dkuegler 908daca
Fixes broken by history rewrite (merge => rebase)
dkuegler 3dbf31d
Fixing problems introduced by incomplete changes in review
dkuegler f2aa773
rename files and standardize file names
dkuegler f4652ff
Fix doc build errors and ruff optimization codes
dkuegler ebee7ad
updated helptexts & formatting
ClePol 189cfc8
documentation and review comments
ClePol 4d2f6df
updated logging in paint_cc_into_pred and added missing docfiles
ClePol 6fb08c5
Improve the left_right masking.
dkuegler d2bee05
Fix the CorpusCallosum documentation
dkuegler 97a7c8a
Remove --qc_output_dir and related functionality to simplify the fast…
dkuegler 576508d
file renaming, removed unused code, documentation update
ClePol 22768a1
fixed commandline texts, parameter and absolute paths
ClePol 4ec8ec5
Rewrite of CCIndex, fixed middle slice selection argument, helptext
ClePol 246fc77
Fix AC-PC localization
dkuegler 0e0f5cf
Rework the generation of transformation matrices and make them more u…
dkuegler 7137ba8
Fix docstrings and formatting in mesh.py
dkuegler 3780abe
Fix ruff errors
dkuegler 5024d0d
updated helptext
ClePol a4c49c0
split cc_mesh class into cc_mesh and cc_contour
ClePol 8bab138
updated cc visualization script with cleaner interface to Mesh, Conto…
ClePol 4f8a5a5
cleaned up visualization script logic, removed unused CC contour code…
ClePol 2c924c8
Add the types file to merge all recurring types of FastSurferCC
dkuegler 005917c
- Fix ruff and documentation errors
dkuegler de1bedf
Rename create_CC_mesh_from_contours to CCMesh.from_contours and make …
dkuegler bbb66c7
Fix surface output
dkuegler adfd523
- Clean up docstrings, typing
dkuegler f3dcdca
fixed label consolidation
ClePol 8757466
visualization & commandline interface bugfixes
ClePol aa427e7
- Fix saving the cc_surf (only saved when thickness_image was requested
dkuegler d850687
Fix and add explanation on how change cc_visualization.py
dkuegler 90ab5d7
Fix doc build error.
dkuegler cd7c6e9
Update CorpusCallosum to use the new lapy.Polygon interface.
dkuegler eb411a5
fixed for subsegmentation and lapy+cc contour
ClePol b3b0de9
added advanced curvature metrics and refactored curvature calculation
ClePol a222374
bump lapy to 1.5.0 (instead of install from github)
dkuegler b536b2f
adressed fixmes, fixed bugs, added warnings
ClePol 1bdb040
adressed review comments
ClePol 3958a97
Fix the types of CCMeasuresDict.thickness_profile and ContourList
dkuegler 3b930b5
Fix endpoint extraction and rotation of offsets (for endpoint extract…
dkuegler 3035c7e
Fix doc and style
dkuegler 16dbb8f
fixed stats (subseg-area, ordering, etc.), improved visualization
ClePol f99adfd
fix 3d visualization
ClePol File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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", | ||
| ] |
ClePol marked this conversation as resolved.
Show resolved
Hide resolved
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| )) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.