From 258c9dc87a8b666bb9377ec71dc984ddbb430c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Thu, 19 Feb 2026 12:49:47 +0100 Subject: [PATCH 1/3] Fix error code of FastSurfer-CC FastSurfer-CC's fastsurfer_cc.py returned with erroror code 0 despite failing some cc surface processing steps. This Commit fixes the error processing of FsatSurfer-CC so that the error code and error message are consistent and robust. CorpusCallosum.fastsurfer_cc.main now does not only return None irrespective of success, but returns 0 if successful and an error string. Also two minor fixes: 1. use BASH_SOURCE[0] instead of $0 in recon_sirf/functions.sh 2. add a missing wrap in run_fastsurfer.sh --- CorpusCallosum/fastsurfer_cc.py | 47 ++++++++++++++++++++++----------- recon_surf/functions.sh | 2 +- run_fastsurfer.sh | 4 +-- 3 files changed, 35 insertions(+), 18 deletions(-) diff --git a/CorpusCallosum/fastsurfer_cc.py b/CorpusCallosum/fastsurfer_cc.py index b05efb32..8e346686 100644 --- a/CorpusCallosum/fastsurfer_cc.py +++ b/CorpusCallosum/fastsurfer_cc.py @@ -599,7 +599,7 @@ def main( softlabels_cc: str | Path | None = None, softlabels_fn: str | Path | None = None, softlabels_background: str | Path | None = None, -) -> None: +) -> Literal[0] | str: """Main pipeline function for corpus callosum analysis. This function performs the complete corpus callosum analysis pipeline including @@ -661,6 +661,15 @@ def main( softlabels_background : str or Path, optional Path to save background soft labels. + Returns + ------- + 0 or str + 0 if successful, otherwise an error message. + + Raises + ------ + This function does not follow the convention "main does not raise exceptions" yet. + Notes ----- The function saves multiple outputs to specified paths or default locations in output_dir: @@ -724,7 +733,7 @@ def main( sys.exit(1) #### setup variables - io_futures = [] + futures = [] # load models device = find_device(device) @@ -772,7 +781,7 @@ def main( # start saving upright volume, this is the image in fsaverage space but not yet oriented via AC-PC if sd.has_attribute("upright_volume"): # upright == fsaverage-aligned - io_futures.append( + futures.append( thread_executor().submit( apply_transform_to_volume, orig, @@ -833,7 +842,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: for i, (attr, name) in enumerate((("background",) * 2, ("cc", "Corpus Callosum"), ("fn", "Fornix"))): if sd.has_attribute(f"cc_softlabels_{attr}"): logger.info(f"Saving {name} softlabels to {sd.filename_by_attribute(f'cc_softlabels_{attr}')}") - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( nib.save, nib.MGHImage(cc_fn_softlabels[..., i], fsaverage_midslab_vox2ras, orig.header), sd.filename_by_attribute(f"cc_softlabels_{attr}"), @@ -847,7 +856,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: _cc_seg_path = sd.filename_by_attribute("cc_segmentation") _cc_seg_path.parent.mkdir(exist_ok=True, parents=True) logger.info(f"Saving CC segmentation to {_cc_seg_path}") - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( nib.save, nib.MGHImage(cc_fn_seg_labels, fsaverage_midslab_vox2ras, orig.header), _cc_seg_path, @@ -871,7 +880,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: contour_smoothing=contour_smoothing, subject_dir=sd, ) - io_futures.extend(slice_io_futures) + futures.extend(slice_io_futures) except Exception as e: logger.error(f"CC morphometry analysis failed: {e}") logger.exception(e) @@ -924,7 +933,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: cc_subseg_midslice = None # if num_threads is not large enough (>1), this might be blocking ; serial_executor runs the function in submit executor = thread_executor() if get_num_threads() > 2 else serial_executor() - io_futures.append(executor.submit( + futures.append(executor.submit( map_softlabels_to_orig, cc_fn_softlabels=cc_fn_softlabels, orig=orig, @@ -1026,7 +1035,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: if sd.has_attribute("cc_mid_measures") and middle_slice_result is not None: sd.filename_by_attribute('cc_mid_measures').parent.mkdir(exist_ok=True, parents=True) - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( save_cc_measures_json, sd.filename_by_attribute('cc_mid_measures'), output_metrics_middle_slice | additional_metrics, @@ -1034,7 +1043,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: if sd.has_attribute("cc_measures"): sd.filename_by_attribute("cc_measures").parent.mkdir(exist_ok=True, parents=True) - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( save_cc_measures_json, sd.filename_by_attribute("cc_measures"), per_slice_output_dict | additional_metrics, @@ -1045,7 +1054,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: if sd.has_attribute("upright_lta"): sd.filename_by_attribute("upright_lta").parent.mkdir(exist_ok=True, parents=True) logger.info(f"Saving LTA to fsaverage space: {sd.filename_by_attribute('upright_lta')}") - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( write_lta, sd.filename_by_attribute("upright_lta"), orig2fsavg_ras2ras, @@ -1061,7 +1070,7 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: fsavg2standardized_ras2ras = fsavg_vox2ras @ \ np.linalg.inv(standardized2orig_vox2vox) @ np.linalg.inv(orig.affine) logger.info(f"Saving LTA to standardized space: {sd.filename_by_attribute('cc_orient_volume_lta')}") - io_futures.append(thread_executor().submit( + futures.append(thread_executor().submit( write_lta, sd.filename_by_attribute("cc_orient_volume_lta"), fsavg2standardized_ras2ras, @@ -1072,14 +1081,20 @@ def _orig2midslab_vox2vox(additional_context: int = 0) -> AffineMatrix4x4: )) # this waits for all io to finish - for fut in io_futures: + return_value: Literal[0] | str = 0 + success_str = "completed successfully in" + for fut in futures: e = fut.exception() if e and isinstance(e, Exception): logger.exception(e) + success_str = "failed after" + if return_value == 0: + return_value = f"Error during saving outputs: {e}" shutdown_executors() duration = (perf_counter_ns() - start) / 1e9 - logger.info(f"CorpusCallosum analysis pipeline completed successfully in {duration:.2f} seconds.") + logger.info(f"CorpusCallosum analysis pipeline {success_str} {duration:.2f} seconds.") + return return_value def init_mgh_header(header: nibabelHeader, header_dict: MGHHeaderDict) -> MGHHeader: @@ -1117,12 +1132,14 @@ def save_cc_measures_json(cc_mid_measure_file: Path, metrics: dict[str, object]) if __name__ == "__main__": + import sys + options = options_parse() # Set up logging if verbose mode is enabled logging.setup_logging(None, options.verbose) # Log to stdout only - main( + sys.exit(main( conf_name=options.conf_name, aseg_name=options.aseg_name, subject_dir=options.subject_dir, @@ -1149,4 +1166,4 @@ def save_cc_measures_json(cc_mid_measure_file: Path, metrics: dict[str, object]) softlabels_cc=options.softlabels_cc, softlabels_fn=options.softlabels_fn, softlabels_background=options.softlabels_background, - ) + )) diff --git a/recon_surf/functions.sh b/recon_surf/functions.sh index db3b073f..801be99b 100644 --- a/recon_surf/functions.sh +++ b/recon_surf/functions.sh @@ -1,6 +1,6 @@ # set the binpath variable -if [[ -z "$FASTSURFER_HOME" ]] ; then binpath="$( cd -- "$(dirname "$0")" >/dev/null 2>&1 ; pwd -P )/" +if [[ -z "$FASTSURFER_HOME" ]] ; then binpath="$( cd -- "$(dirname "${BASH_SOURCE[0]}")" >/dev/null 2>&1 ; pwd -P )/" else binpath="$FASTSURFER_HOME/recon_surf/" fi export binpath diff --git a/run_fastsurfer.sh b/run_fastsurfer.sh index 15698cad..3bafea86 100755 --- a/run_fastsurfer.sh +++ b/run_fastsurfer.sh @@ -1009,7 +1009,7 @@ then cmd=($python "$fastsurfercnndir/reduce_to_aseg.py" -i "$asegdkt_segfile" -o "$aseg_segfile" --outmask "$mask_name" --fixwm) echo_quoted "${cmd[@]}" | tee -a "$seg_log" - "${cmd[@]}" | tee -a "$seg_log" + "${wrap[@]}" "${cmd[@]}" | tee -a "$seg_log" exit_code="${PIPESTATUS[0]}" if [[ "${exit_code}" != 0 ]] then @@ -1275,7 +1275,7 @@ then --conformed_name "$conformed_name" --cereb_segfile "$cereb_segfile" --async_io --batch_size "$batch_size" --viewagg_device "$viewagg" --device "$device" --threads "$threads_seg" "${cereb_flags[@]}") # specify the subject dir $sd, if cereb_segfile explicitly starts with it - if [[ "$sd" == "${cereb_segfile:0:${#sd}}" ]] ; then cmd=("${cmd[@]}" --sd "$sd"); fi + if [[ "$sd" == "${cereb_segfile:0:${#sd}}" ]] ; then cmd+=(--sd "$sd"); fi if [[ "$native_image" != "false" ]] ; then cmd+=(--orientation native --image_size fov --vox_size none) ; fi echo_quoted "${cmd[@]}" | tee -a "$seg_log" "${wrap[@]}" "${cmd[@]}" # no tee, directly logging to $seg_log From 448115568cc45a16afd0b70f0177fb6988a384d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Wed, 25 Feb 2026 16:49:14 +0100 Subject: [PATCH 2/3] - Fix/remove extra "===+" in FastSurfer-CC documentation. - Add a heading to the Parser self-documentation --- doc/scripts/fastsurfer_cc.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/doc/scripts/fastsurfer_cc.rst b/doc/scripts/fastsurfer_cc.rst index cfa085b9..561e3196 100644 --- a/doc/scripts/fastsurfer_cc.rst +++ b/doc/scripts/fastsurfer_cc.rst @@ -11,7 +11,11 @@ CorpusCallosum: fastsurfer_cc.py .. include:: ../../CorpusCallosum/README.md :parser: fix_links.parser - :start-line: 1 + :start-line: 2 + +Full command-line interface +--------------------------- +The following section provides a detailed overview of the command-line interface for the FastSurfer-CC pipeline, including all available flags and options. .. argparse:: :module: CorpusCallosum.fastsurfer_cc @@ -21,9 +25,9 @@ CorpusCallosum: fastsurfer_cc.py Quality Control --------------- The pipeline can produce a dedicated quality control image, showing the CC contour, AC/PC landmarks and thickness estimation. -For this use the --qc_image flag. -Additionally, the surface outputs, e.g. --thickness_image, can be used to visualize the CC thickness and also inform quality control. -Finally, to confirm the alignment of the CC on the mid-sagittal plane, we can output the upright volume with --upright_volume flag. +For this use the ``--qc_image`` flag. +Additionally, the surface outputs, e.g. ``--thickness_image``, can be used to visualize the CC thickness and also inform quality control. +Finally, to confirm the alignment of the CC on the mid-sagittal plane, we can output the upright volume with ``--upright_volume`` flag. In this image the mid-sagittal plane is at voxel coordinate 128 in the LR direction. An example call with all quality control outputs is: @@ -36,7 +40,7 @@ An example call with all quality control outputs is: Custom Subdivision Schemes -------------------------- -The pipeline supports custom subdivision schemes for the corpus callosum with the --subdivisions flag. +The pipeline supports custom subdivision schemes for the corpus callosum with the ``--subdivisions`` flag. The fractions are relative to the total length of the corpus callosum (midline length). The default is to use the shape-based subdivision scheme (recommended) and the Hofer-Frahms convention. From 384b48adac56e261ecb81c76f7545d10e3002246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?David=20K=C3=BCgler?= Date: Wed, 25 Feb 2026 17:45:57 +0100 Subject: [PATCH 3/3] Move sys.exit calls out of the main function in fastsurfer_cc.py --- CorpusCallosum/fastsurfer_cc.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/CorpusCallosum/fastsurfer_cc.py b/CorpusCallosum/fastsurfer_cc.py index 8e346686..3397e807 100644 --- a/CorpusCallosum/fastsurfer_cc.py +++ b/CorpusCallosum/fastsurfer_cc.py @@ -690,8 +690,6 @@ def main( """ start = perf_counter_ns() - import sys - if subdivisions is None: subdivisions = [1 / 6, 1 / 2, 2 / 3, 3 / 4] @@ -729,8 +727,9 @@ def main( # Validate subdivision fractions if any(i < 0 or i > 1 for i in subdivisions): - logger.error(f"Subdivision fractions must be between 0 and 1, but got: {subdivisions}") - sys.exit(1) + error_message = f"Subdivision fractions must be between 0 and 1, but got: {subdivisions}" + logger.error(error_message) + return error_message #### setup variables futures = [] @@ -752,8 +751,9 @@ def main( logger.info("Robust rescaling of input intensities.") orig = conform(orig, vox_size=None, img_size=None, orientation=None) if not np.allclose(_orig_affine, orig.affine): - logger.error("Conforming the image should not change the affine, but it did!") - sys.exit(1) + error_message = "Conforming the image should not change the affine, but it did!" + logger.error(error_message) + return error_message # 5 mm around the midplane (guaranteed to be aligned RAS by as_closest_canonical) vox_size_ras: tuple[float, float, float] = nib.as_closest_canonical(orig).header.get_zooms() @@ -771,8 +771,9 @@ def main( aseg_img = cast(nibabelImage, _aseg_fut.result()) if not np.allclose(aseg_img.affine, orig.affine): - logger.error("Input MRI and segmentation are not aligned! Please check your input files.") - sys.exit(1) + error_message = "Input MRI and segmentation are not aligned! Please check your input files." + logger.error(error_message) + return error_message logger.info("Performing centroid registration to fsaverage space") orig2fsavg_vox2vox, orig2fsavg_ras2ras, fsavg_vox2ras, _fsavg_header_dict = register_centroids_to_fsavg(aseg_img)