From eb4c7390b5bbe849e7aefc832e023c7fbf99489c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Thu, 26 Feb 2026 12:33:08 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Here's=20a=20summary=20of=20all=20changes?= =?UTF-8?q?=20made=20to=20bump=20whippersnappy=20from=201.3=20to=202.0:=20?= =?UTF-8?q?1.=20pyproject.toml=20=E2=80=94=20Version=20pin=20updated=20whi?= =?UTF-8?q?ppersnappy>=3D1.3.1=20=E2=86=92=20whippersnappy>=3D2.0=202.=20r?= =?UTF-8?q?equirements.mac.txt=20=E2=80=94=20Version=20pin=20updated=20whi?= =?UTF-8?q?ppersnappy>=3D1.3.1=20=E2=86=92=20whippersnappy>=3D2.0=203.=20r?= =?UTF-8?q?un=5Ffastsurfer.sh=20=E2=80=94=20Removed=20xvfb=20orchestration?= =?UTF-8?q?=20(lines=20767=E2=80=93794=20removed,=20line=201184=20simplifi?= =?UTF-8?q?ed)=20Removed=20the=20entire=20maybe=5Fxvfb=20block:=20xvfb-run?= =?UTF-8?q?=20detection,=20the=20glfw/OpenGL/whippersnappy.core=20import?= =?UTF-8?q?=20test,=20and=20all=20associated=20warning=20messages.=20Whipp?= =?UTF-8?q?ersnappy=202.0=20renders=20headlessly=20via=20native=20EGL=20?= =?UTF-8?q?=E2=80=94=20no=20virtual=20framebuffer=20needed.=20Removed=20"$?= =?UTF-8?q?{maybe=5Fxvfb[@]}"=20prefix=20from=20the=20CC=20module=20comman?= =?UTF-8?q?d=20invocation.=20Removed=20the=20obsolete=20comment=20about=20?= =?UTF-8?q?xvfb-run=20wrapping.=204.=20tools/Docker/Dockerfile=20=E2=80=94?= =?UTF-8?q?=20Slimmed=20runtime=20dependencies=20whippersnappy=5Fopengl=5F?= =?UTF-8?q?deps=3D"xvfb=20libglib2.0-0=20libxkbcommon-x11-0=20libgl1=20lib?= =?UTF-8?q?egl1=20libfontconfig1=20libdbus-1-3"=20=E2=86=92=20whippersnapp?= =?UTF-8?q?y=5Fegl=5Fdeps=3D"libegl1=20libgl1=20libfontconfig1"=20Removed:?= =?UTF-8?q?=20xvfb=20(virtual=20framebuffer,=20replaced=20by=20native=20EG?= =?UTF-8?q?L),=20libglib2.0-0=20(X11/GUI=20stack),=20libxkbcommon-x11-0=20?= =?UTF-8?q?(X11=20keyboard),=20libdbus-1-3=20(D-Bus,=20X11-related)=20Kept?= =?UTF-8?q?:=20libegl1=20(EGL=20rendering),=20libgl1=20(OpenGL),=20libfont?= =?UTF-8?q?config1=20(font=20rendering=20for=20captions/colorbars)=205.=20?= =?UTF-8?q?CorpusCallosum/shape/mesh.py=20=E2=80=94=20Updated=20import=20a?= =?UTF-8?q?nd=20API=20call=20Removed=20import=20OpenGL.GL=20pre-check=20(w?= =?UTF-8?q?hippersnappy=202.0=20manages=20its=20own=20GL=20context)=20Chan?= =?UTF-8?q?ged=20from=20whippersnappy.core=20import=20snap1=20=E2=86=92=20?= =?UTF-8?q?from=20whippersnappy=20import=20snap1=20(new=20top-level=20impo?= =?UTF-8?q?rt)=20Removed=20the=20except=20Exception=20handler=20about=20xv?= =?UTF-8?q?fb=20(no=20longer=20relevant=20with=20EGL)=20Updated=20snap1()?= =?UTF-8?q?=20call:=20positional=20first=20arg=20=E2=86=92=20mesh=3D,=20ov?= =?UTF-8?q?erlaypath=3D=20=E2=86=92=20overlay=3D=20(new=20API=20parameter?= =?UTF-8?q?=20names)=206.=20CorpusCallosum/cc=5Fvisualization.py=20?= =?UTF-8?q?=E2=80=94=20Updated=20version=20references=20Help=20text=20and?= =?UTF-8?q?=20warning=20messages:=20whippersnappy>=3D1.3.1=20=E2=86=92=20w?= =?UTF-8?q?hippersnappy>=3D2.0=207.=20CorpusCallosum/shape/postprocessing.?= =?UTF-8?q?py=20=E2=80=94=20Updated=20error=20messages=20ImportError=20mes?= =?UTF-8?q?sage:=20removed=20"glfw=20or=20OpenGL"=20(whippersnappy=20handl?= =?UTF-8?q?es=20these=20internally)=20Generic=20Exception=20message:=20rep?= =?UTF-8?q?laced=20xvfb=20guidance=20with=20EGL/libegl1=20guidance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CorpusCallosum/cc_visualization.py | 6 ++--- CorpusCallosum/shape/mesh.py | 13 ++--------- CorpusCallosum/shape/postprocessing.py | 7 +++--- pyproject.toml | 2 +- requirements.mac.txt | 2 +- run_fastsurfer.sh | 31 +------------------------- tools/Docker/Dockerfile | 4 ++-- 7 files changed, 13 insertions(+), 52 deletions(-) diff --git a/CorpusCallosum/cc_visualization.py b/CorpusCallosum/cc_visualization.py index a793ba440..a1404cec6 100644 --- a/CorpusCallosum/cc_visualization.py +++ b/CorpusCallosum/cc_visualization.py @@ -39,7 +39,7 @@ def make_parser() -> argparse.ArgumentParser: "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)", + "cc_mesh_snap.png - Screenshot/snapshot of the 3D mesh (requires whippersnappy>=2.0)", metavar="OUTPUT_DIR" ) parser.add_argument( @@ -232,8 +232,8 @@ def main( 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` !") + logger.warning("The cc_visualization script requires whippersnappy>=2.0 to makes screenshots, install with " + "`pip install whippersnappy>=2.0` !") return 0 if __name__ == "__main__": diff --git a/CorpusCallosum/shape/mesh.py b/CorpusCallosum/shape/mesh.py index 8dcdfa53f..8873673d9 100644 --- a/CorpusCallosum/shape/mesh.py +++ b/CorpusCallosum/shape/mesh.py @@ -538,9 +538,7 @@ def snap_cc_picture( 3. Cleans up temporary files after use. """ try: - # Dummy import of OpenCL to ensure it's available for whippersnappy - import OpenGL.GL # noqa: F401 - from whippersnappy.core import snap1 + from whippersnappy import snap1 except ImportError as e: # whippersnappy not installed raise ImportError( @@ -548,13 +546,6 @@ def snap_cc_picture( f"Please install {e.name}!", name=e.name, path=e.path ) from None - except Exception as e: - # Catch all other types of errors, - raise RuntimeError( - "Could not import OpenGL or whippersnappy. The snap_cc_picture method of CCMesh requires OpenGL and " - "whippersnappy to render the QC thickness image. On headless servers, this also requires a virtual " - "framebuffer like xvfb.", - ) from e self.__make_parent_folder(output_path) # Skip snapshot if there are no faces if len(self.t) == 0: @@ -581,7 +572,7 @@ def snap_cc_picture( with suppress_stdout(): snap1( fssurf_file, - overlaypath=overlay_file, + overlay=overlay_file, view=None, viewmat=self.__create_cc_viewmat(), width=3 * 500, diff --git a/CorpusCallosum/shape/postprocessing.py b/CorpusCallosum/shape/postprocessing.py index 6ad7b4210..0ce680a7e 100644 --- a/CorpusCallosum/shape/postprocessing.py +++ b/CorpusCallosum/shape/postprocessing.py @@ -301,14 +301,13 @@ def _zip_failed(it_idx, it_affine, it_result): cc_surf_generated = True except (ImportError, ModuleNotFoundError) as e: logger.error( - "The thickness image was not generated because whippersnappy, glfw or OpenGL are not installed." + "The thickness image was not generated because whippersnappy is not installed." ) logger.exception(e) except Exception as e: logger.error( - "The thickness image was not generated (see below). On headless Linux systems or if the " - "x-server cannot/should not be accessed due to other reasons, xvfb-run may be used to provide " - "a virtual framebuffer for offscreen rendering." + "The thickness image was not generated (see below). Please ensure that EGL " + "libraries (libegl1) are available for headless rendering." ) logger.exception(e) if not cc_surf_generated and wants_output("cc_surf"): diff --git a/pyproject.toml b/pyproject.toml index 5b5bed332..da1867877 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ [project.optional-dependencies] qc = [ - 'whippersnappy>=1.3.1', + 'whippersnappy>=2.0', ] doc = [ 'fastsurfer[qc]', diff --git a/requirements.mac.txt b/requirements.mac.txt index fc713c6af..6d78c3dfa 100644 --- a/requirements.mac.txt +++ b/requirements.mac.txt @@ -19,5 +19,5 @@ yacs>=0.1.8 monai>=1.4.0 meshpy>=2025.1.1 pyrr>=0.10.3 -whippersnappy>=1.3.1 +whippersnappy>=2.0 pip>=25.0 \ No newline at end of file diff --git a/run_fastsurfer.sh b/run_fastsurfer.sh index 15698cad8..05e6795c3 100755 --- a/run_fastsurfer.sh +++ b/run_fastsurfer.sh @@ -764,34 +764,6 @@ then fi fi -maybe_xvfb=() -# check if we are running on a headless system (CC QC needs a (virtual) display that support OpenGL) -if [[ "$run_seg_pipeline" == "true" ]] && [[ "$run_cc_module" == "true" ]] && \ - [[ "${cc_flags[*]}" =~ --thickness_image ]] -then - # if we have xvfb-run, we can use it to provide a virtual display - if [[ -n "$(which xvfb-run)" ]] ; then maybe_xvfb=("xvfb-run" "-a") ; fi - - # try loading opengl, if this is successful we are fine - py_opengltest="import sys ; import glfw ; import whippersnappy.core ; sys.exit(1-glfw.init())" - opengl_error_message="$("${maybe_xvfb[@]}" $python -c "$py_opengltest" 2>&1 > /dev/null)" - exit_code="$?" - if [[ "$exit_code" != "0" ]] - then - # if we cannot import OpenGL or whippersnappy, its an environment installation issue - if [[ "$opengl_error_message" =~ "ModuleNotFoundError" ]] || [[ "$opengl_error_message" =~ "ImportError" ]] - then - echo "WARNING: The --qc_snap option of the corpus callosum module requires the Python packages PyOpenGL, glfw and" - echo " whippersnappy to be installed, but python could not import those three. Please install them and their" - echo " dependencies via 'pip install pyopengl glfw whippersnappy'." - else - echo "WARNING: The --qc_snap option of the corpus callosum module requires OpenGL support, but we could not" - echo " create OpenGL handles. For Linux headless systems, you may install xvfb-run to provide a virtual display." - fi - echo " FastSurfer will not fail due to the unavailability of OpenGL, but some QC snapshots (rendered thickness" - echo " image) will not be created." - fi -fi if [[ "$run_surf_pipeline" == "true" ]] && [[ "$native_image" != "false" ]] then @@ -1181,10 +1153,9 @@ then # note: callosum manedit currently only affects inpainting and not internal FastSurferCC processing (surfaces etc) callosum_seg_manedit="$(add_file_suffix "$callosum_seg" "manedit")" # generate callosum segmentation, mesh, shape and downstream measure files - cmd=("${maybe_xvfb[@]}" $python "$CorpusCallosumDir/fastsurfer_cc.py" --sd "$sd" --sid "$subject" + cmd=($python "$CorpusCallosumDir/fastsurfer_cc.py" --sd "$sd" --sid "$subject" "--threads" "$threads_seg" "--conformed_name" "$conformed_name" "--aseg_name" "$asegdkt_segfile" "--segmentation_in_orig" "$callosum_seg" "${cc_flags[@]}") - # if we are trying to create the thickness image in a headless setting, wrap call in xvfb-run echo_quoted "${cmd[@]}" | tee -a "$seg_log" "${wrap[@]}" "${cmd[@]}" 2>&1 | tee -a "$seg_log" exit_code=${PIPESTATUS[0]} diff --git a/tools/Docker/Dockerfile b/tools/Docker/Dockerfile index b0295c905..8cff414ce 100644 --- a/tools/Docker/Dockerfile +++ b/tools/Docker/Dockerfile @@ -272,11 +272,11 @@ RUN < Date: Thu, 26 Feb 2026 12:55:54 +0100 Subject: [PATCH 2/4] =?UTF-8?q?Added=20a=20validation=20check=20(lines=207?= =?UTF-8?q?67-801=20in=20run=5Ffastsurfer.sh)=20that:=20=E2=97=A6=20Detect?= =?UTF-8?q?s=20if=20--thickness=5Fimage=20flag=20is=20present=20in=20cc=5F?= =?UTF-8?q?flags=20(which=20is=20set=20when=20the=20user=20uses=20--qc=5Fs?= =?UTF-8?q?nap)=20=E2=97=A6=20Checks=20whether=20the=20whippersnappy=20Pyt?= =?UTF-8?q?hon=20package=20is=20installed=20and=20>2.0=20=E2=97=A6=20Raise?= =?UTF-8?q?s=20an=20error=20and=20gives=20instruction=20if=20this=20is=20n?= =?UTF-8?q?ot=20the=20case.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- run_fastsurfer.sh | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/run_fastsurfer.sh b/run_fastsurfer.sh index 05e6795c3..7d4007fcd 100755 --- a/run_fastsurfer.sh +++ b/run_fastsurfer.sh @@ -764,6 +764,41 @@ then fi fi +# Check if --thickness_image is in cc_flags and whippersnappy version is >= 2.0 +if [[ "$run_seg_pipeline" == "true" ]] && [[ "$run_cc_module" == "true" ]] && \ + [[ "${cc_flags[*]}" == *"--thickness_image"* ]] +then + # Check if whippersnappy is installed and version is >= 2.0 + whippersnappy_check=$($python -c " +try: + import whippersnappy as wspy + from packaging.version import parse + print('OK' if parse(wspy.__version__) >= parse('2.0') else ('OLD_VERSION:' + wspy.__version__)) +except ImportError: + print('NOT_INSTALLED') +except Exception as e: + print('ERROR:' + str(e)) +" 2>&1) + + if [[ "$whippersnappy_check" != "OK" ]] + then + if [[ "$whippersnappy_check" == "NOT_INSTALLED" ]] + then + echo "ERROR: The --qc_snap flag requires the 'whippersnappy' package (version >= 2.0) to generate the qc" + echo " thickness image, but whippersnappy is not installed in your Python environment." + elif [[ "$whippersnappy_check" == OLD_VERSION:* ]] + then + installed_version="${whippersnappy_check#OLD_VERSION:}" + echo "ERROR: The --qc_snap flag requires whippersnappy version >= 2.0 to generate the qc thickness image," + echo " but you only have version $installed_version installed." + else + echo "ERROR: Failed to check whippersnappy installation: $whippersnappy_check" + fi + echo " Please install or upgrade whippersnappy with one of the following commands:" + echo " pip install 'whippersnappy>=2.0'" + exit 1 + fi +fi if [[ "$run_surf_pipeline" == "true" ]] && [[ "$native_image" != "false" ]] then From 84e3bdfe7d1128c49b14a9b25700ff5a53664c5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Thu, 26 Feb 2026 14:09:34 +0100 Subject: [PATCH 3/4] Decouple the generation of the thickness image and the freesurfer surface and overlay files. Use direct passing of the surface and overlay data instead of writing the files to disc first. Remove and simplify commands, arguments and the like that are a relict of older whippersnappy versions, now fully requiring whippersnappy 2. --- CorpusCallosum/cc_visualization.py | 18 ++++- CorpusCallosum/shape/mesh.py | 106 +++++++++---------------- CorpusCallosum/shape/postprocessing.py | 33 +++----- 3 files changed, 65 insertions(+), 92 deletions(-) diff --git a/CorpusCallosum/cc_visualization.py b/CorpusCallosum/cc_visualization.py index a1404cec6..ef10f30ea 100644 --- a/CorpusCallosum/cc_visualization.py +++ b/CorpusCallosum/cc_visualization.py @@ -3,12 +3,15 @@ from pathlib import Path from typing import Literal +import nibabel as nib import numpy as np +from nibabel.affines import apply_affine 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 +from FastSurferCNN.utils.lta import read_lta logger = get_logger(__name__) @@ -213,6 +216,17 @@ def main( # 3D visualization cc_mesh = CCMesh.from_contours(contours, smooth=0) + if Path(output_dir / "mri" / "upright.mgz").exists(): + header = nib.load(output_dir / "mri" / "upright.mgz").header + # we need to get the upright image header, which is the same as cc_up.lta applied to orig. + elif Path(template_dir / "mri/orig.mgz").exists() and Path(template_dir / "mri/transforms/cc_up.lta").exists(): + image = nib.load(template_dir / "mri" / "orig.mgz") + lta_mat = read_lta(template_dir / "mri/transforms/cc_up.lta")["lta"] + image.affine = apply_affine(lta_mat, image.affine) + header = image.header + else: + header = None + plot_kwargs = dict( colormap=colormap, color_range=color_range, @@ -225,11 +239,11 @@ def main( 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")) + cc_mesh.write_fssurf(str(output_dir / "cc_mesh.fssurf"), image=header) 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")) + cc_mesh.snap_cc_picture(str(output_dir / "cc_mesh_snap.png"), ref_header=header) logger.info(f"Writing 3D snapshot image to {output_dir / 'cc_mesh_snap.png'}") except RuntimeError: logger.warning("The cc_visualization script requires whippersnappy>=2.0 to makes screenshots, install with " diff --git a/CorpusCallosum/shape/mesh.py b/CorpusCallosum/shape/mesh.py index 8873673d9..eeceb1971 100644 --- a/CorpusCallosum/shape/mesh.py +++ b/CorpusCallosum/shape/mesh.py @@ -27,7 +27,7 @@ import FastSurferCNN.utils.logging as logging from CorpusCallosum.shape.contour import CCContour from CorpusCallosum.shape.thickness import make_mesh_from_contour -from FastSurferCNN.utils import AffineMatrix4x4, nibabelImage +from FastSurferCNN.utils import AffineMatrix4x4, nibabelHeader, nibabelImage from FastSurferCNN.utils.common import suppress_stdout, update_docstring try: @@ -478,11 +478,12 @@ def __create_cc_viewmat() -> "Matrix44": - -8 degrees around z-axis 3. Adds a small translation for better centering """ + from whippersnappy.gl.views import ViewType, get_view_matrix if not HAS_PYRR: raise ImportError("Pyrr not installed, install pyrr with `pip install pyrr`.") - viewLeft = np.array([[0, 0, -1, 0], [-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1]]) # left w top up // right + viewLeft = get_view_matrix(ViewType.LEFT) # left w top up // right transl = Matrix44.from_translation((0, 0, 0.4)) viewmat = transl * viewLeft @@ -503,9 +504,7 @@ def __create_cc_viewmat() -> "Matrix44": def snap_cc_picture( self, output_path: Path | str, - fssurf_file: Path | str | None = None, - overlay_file: Path | str | None = None, - ref_image: Path | str | nibabelImage | None = None, + ref_header: Path | str | nibabelHeader | None = None, ) -> None: """Snap a picture of the corpus callosum mesh. @@ -513,32 +512,17 @@ def snap_cc_picture( ---------- output_path : Path, str Path where to save the snapshot image. - fssurf_file : Path, str, optional - Path to a FreeSurfer surface file to use for the snapshot. - If None, the mesh is saved to a temporary file. - overlay_file : Path, str, optional - Path to a FreeSurfer overlay file to use for the snapshot. - If None, the mesh is saved to a temporary file. - ref_image : Path, str, nibabelImage, optional + ref_header : Path, str, nibabelImage, optional Path to reference image to use for tkr creation. If None, ignores the file for saving. Raises ------ Warning If the mesh has no faces and cannot create a snapshot. - - Notes - ----- - The function: - 1. Creates temporary files for mesh and overlay data if needed. - 2. Uses whippersnappy to create a snapshot with: - - Custom view matrix for standard orientation. - - Ambient lighting and colorbar settings. - - Thickness overlay if available. - 3. Cleans up temporary files after use. """ + from packaging.version import parse try: - from whippersnappy import snap1 + import whippersnappy except ImportError as e: # whippersnappy not installed raise ImportError( @@ -546,56 +530,44 @@ def snap_cc_picture( f"Please install {e.name}!", name=e.name, path=e.path ) from None - self.__make_parent_folder(output_path) + from nibabel.affines import apply_affine + + if parse(whippersnappy.__version__) < parse("2.0.0"): + raise ImportError( + f"The snap_cc_picture method of CCMesh requires whippersnappy>=2.0.0, but version " + f"{whippersnappy.__version__} is installed. Please upgrade whippersnappy to version 2.0.0 or higher!", + name="whippersnappy", path=None + ) # Skip snapshot if there are no faces if len(self.t) == 0: logger.warning("Cannot create snapshot - no faces in mesh") return - # create temp file - if fssurf_file: - fssurf_file = Path(fssurf_file) - else: - fssurf_file = tempfile.NamedTemporaryFile(suffix=".fssurf", delete=True).name - - ref_image_arg = str(ref_image) if isinstance(ref_image, (Path, str)) else ref_image - self.write_fssurf(fssurf_file, image=ref_image_arg) + self.__make_parent_folder(output_path) - if overlay_file: - overlay_file = Path(overlay_file) + if ref_header is not None: + v = apply_affine(ref_header.get_vox2ras_tkr(), self.v) else: - overlay_file = tempfile.NamedTemporaryFile(suffix=".w", delete=True).name - # Write thickness values in FreeSurfer '*.w' overlay format - self.write_morph_data(overlay_file) - - try: - with suppress_stdout(): - snap1( - fssurf_file, - overlay=overlay_file, - view=None, - viewmat=self.__create_cc_viewmat(), - width=3 * 500, - height=3 * 300, - outpath=output_path, - ambient=0.6, - colorbar_scale=0.5, - colorbar_y=0.88, - colorbar_x=0.19, - brain_scale=2.1, - fthresh=0, - caption="Corpus Callosum thickness (mm)", - caption_y=0.85, - caption_x=0.17, - caption_scale=0.5, - ) - except Exception as e: - raise e from None - - if fssurf_file and hasattr(fssurf_file, "close"): - fssurf_file.close() - if overlay_file and hasattr(overlay_file, "close"): - overlay_file.close() + v = self.v + whippersnappy.snap1( + mesh=(v, self.t), + overlay=self.mesh_vertex_colors, + view=None, + viewmat=self.__create_cc_viewmat(), + width=3 * 500, + height=3 * 300, + outpath=str(output_path), + ambient=0.6, + colorbar_scale=0.5, + colorbar_y=0.88, + colorbar_x=0.19, + brain_scale=2.1, + fthresh=0, + caption="Corpus Callosum thickness (mm)", + caption_y=0.85, + caption_x=0.17, + caption_scale=0.5, + ) def smooth_(self, iterations: int = 1) -> None: """Smooth the mesh while preserving the z-coordinates. @@ -665,7 +637,7 @@ def to_vox_coordinates( return new_object @update_docstring(parent_doc=TriaMesh.write_fssurf.__doc__) - def write_fssurf(self, filename: Path | str, image: str | nibabelImage | None = None) -> None: + def write_fssurf(self, filename: Path | str, image: str | nibabelImage | nibabelHeader | None = None) -> None: """{parent_doc} Also creates parent directory if needed before writing the file. """ diff --git a/CorpusCallosum/shape/postprocessing.py b/CorpusCallosum/shape/postprocessing.py index 0ce680a7e..7bf6c942f 100644 --- a/CorpusCallosum/shape/postprocessing.py +++ b/CorpusCallosum/shape/postprocessing.py @@ -272,7 +272,7 @@ def _zip_failed(it_idx, it_affine, it_result): logger.info(f"Saving vtk file to {vtk_file_path}") io_futures.append(run(cc_mesh.write_vtk, vtk_file_path)) - if wants_output("cc_thickness_overlay") and not wants_output("cc_thickness_image"): + if wants_output("cc_thickness_overlay"): overlay_file_path = output_path("cc_thickness_overlay") logger.info(f"Saving overlay file to {overlay_file_path}") io_futures.append(run(cc_mesh.write_morph_data, overlay_file_path)) @@ -284,36 +284,23 @@ def _zip_failed(it_idx, it_affine, it_result): # the mesh is generated in upright coordinates, so we need to also transform to orig coordinates # Mesh is fsavg_midplane (RAS); we need to transform to voxel coordinates # fsavg ras is also on the midslice, so this is fine and we multiply in the IA and SP offsets - cc_mesh = cc_mesh.to_vox_coordinates(mesh_ras2vox=np.linalg.inv(fsavg_vox2ras @ orig2fsavg_vox2vox)) - cc_surf_generated = False + cc_mesh_orig = cc_mesh.to_vox_coordinates(mesh_ras2vox=np.linalg.inv(fsavg_vox2ras @ orig2fsavg_vox2vox)) + if wants_output("cc_surf"): + surf_file_path = output_path("cc_surf") + logger.info(f"Saving surf file to {surf_file_path}") + io_futures.append(run(cc_mesh_orig.write_fssurf, surf_file_path, image=upright_header)) + if wants_output("cc_thickness_image"): - # this will also write overlay and surface thickness_image_path = output_path("cc_thickness_image") logger.info(f"Saving thickness image to {thickness_image_path}") - kwargs = { - "fssurf_file": output_path("cc_surf") if wants_output("cc_surf") else None, - "overlay_file": output_path("cc_thickness_overlay") - if wants_output("cc_thickness_overlay") else None, - "ref_image": upright_img, - } try: - cc_mesh.snap_cc_picture(thickness_image_path, **kwargs) - cc_surf_generated = True - except (ImportError, ModuleNotFoundError) as e: - logger.error( - "The thickness image was not generated because whippersnappy is not installed." - ) - logger.exception(e) + cc_mesh_orig.snap_cc_picture(thickness_image_path, ref_header=upright_header) except Exception as e: logger.error( - "The thickness image was not generated (see below). Please ensure that EGL " - "libraries (libegl1) are available for headless rendering." + "Generation of the thickness image failed (see below). Please ensure that whippersnappy and " + "(for headless rendering) EGL libraries (libegl1) are available." ) logger.exception(e) - if not cc_surf_generated and wants_output("cc_surf"): - surf_file_path = output_path("cc_surf") - logger.info(f"Saving surf file to {surf_file_path}") - io_futures.append(run(cc_mesh.write_fssurf, str(surf_file_path), image=upright_img)) if not slice_cc_measures: logger.error("Error: No valid slices were found for postprocessing") From 027b0c3d0079e1ef7b7cea681762d1956241ac35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Thu, 26 Feb 2026 18:48:17 +0100 Subject: [PATCH 4/4] Fix ruff warnings --- CorpusCallosum/shape/mesh.py | 3 +-- CorpusCallosum/shape/postprocessing.py | 3 --- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/CorpusCallosum/shape/mesh.py b/CorpusCallosum/shape/mesh.py index eeceb1971..450ee75c2 100644 --- a/CorpusCallosum/shape/mesh.py +++ b/CorpusCallosum/shape/mesh.py @@ -12,7 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -import tempfile from pathlib import Path from typing import TypeVar @@ -28,7 +27,7 @@ from CorpusCallosum.shape.contour import CCContour from CorpusCallosum.shape.thickness import make_mesh_from_contour from FastSurferCNN.utils import AffineMatrix4x4, nibabelHeader, nibabelImage -from FastSurferCNN.utils.common import suppress_stdout, update_docstring +from FastSurferCNN.utils.common import update_docstring try: from pyrr import Matrix44 diff --git a/CorpusCallosum/shape/postprocessing.py b/CorpusCallosum/shape/postprocessing.py index 7bf6c942f..6f8748b48 100644 --- a/CorpusCallosum/shape/postprocessing.py +++ b/CorpusCallosum/shape/postprocessing.py @@ -278,9 +278,6 @@ def _zip_failed(it_idx, it_affine, it_result): io_futures.append(run(cc_mesh.write_morph_data, overlay_file_path)) if any(wants_output(f"cc_{n}") for n in ("thickness_image", "surf")): - import nibabel as nib - up_data: Image3d[np.uint8] = np.empty(upright_header["dims"][:3], dtype=upright_header.get_data_dtype()) - upright_img = nib.MGHImage(up_data, fsavg_vox2ras, upright_header) # the mesh is generated in upright coordinates, so we need to also transform to orig coordinates # Mesh is fsavg_midplane (RAS); we need to transform to voxel coordinates # fsavg ras is also on the midslice, so this is fine and we multiply in the IA and SP offsets