Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
64 changes: 41 additions & 23 deletions CorpusCallosum/fastsurfer_cc.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand All @@ -681,8 +690,6 @@ def main(
"""
start = perf_counter_ns()

import sys

if subdivisions is None:
subdivisions = [1 / 6, 1 / 2, 2 / 3, 3 / 4]

Expand Down Expand Up @@ -720,11 +727,12 @@ 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
io_futures = []
futures = []

# load models
device = find_device(device)
Expand All @@ -743,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()
Expand All @@ -762,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)
Expand All @@ -772,7 +782,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,
Expand Down Expand Up @@ -833,7 +843,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}"),
Expand All @@ -847,7 +857,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,
Expand All @@ -871,7 +881,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)
Expand Down Expand Up @@ -924,7 +934,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,
Expand Down Expand Up @@ -1026,15 +1036,15 @@ 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,
))

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,
Expand All @@ -1045,7 +1055,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,
Expand All @@ -1061,7 +1071,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,
Expand All @@ -1072,14 +1082,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:
Expand Down Expand Up @@ -1117,12 +1133,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,
Expand All @@ -1149,4 +1167,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,
)
))
14 changes: 9 additions & 5 deletions doc/scripts/fastsurfer_cc.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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.

Expand Down
2 changes: 1 addition & 1 deletion recon_surf/functions.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 2 additions & 2 deletions run_fastsurfer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down