From aa9c4dee5138faf196eebe9cb74b63bdec869255 Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Wed, 8 Apr 2026 21:57:49 -0400 Subject: [PATCH 1/2] Disable gzip compression for float NIfTI intermediates Write intermediate float volumes as uncompressed .nii instead of .nii.gz to avoid gzip overhead on data that is read back immediately. Integer and binary masks are left compressed since they compress efficiently. - Replace fslsplit with nibabel four_to_three in split_4d (also removes a container call) - Use .nii for per-volume ANTs outputs in resampling and longitudinal transform loops - Use .nii for motion reference slice, masking float intermediates, and distortion correction float intermediates Closes #252 --- src/rbc/core/common.py | 22 ++++++++++++++-------- src/rbc/core/functional/distortion.py | 4 ++-- src/rbc/core/functional/masking.py | 10 +++++----- src/rbc/core/functional/motion.py | 2 +- src/rbc/core/functional/resampling.py | 4 ++-- src/rbc/core/longitudinal/transform.py | 2 +- 6 files changed, 25 insertions(+), 19 deletions(-) diff --git a/src/rbc/core/common.py b/src/rbc/core/common.py index b7c1b97a..559a1885 100644 --- a/src/rbc/core/common.py +++ b/src/rbc/core/common.py @@ -10,13 +10,14 @@ from typing import TYPE_CHECKING import nibabel as nib -from niwrap import afni, fsl +from niwrap import afni if TYPE_CHECKING: from collections.abc import Sequence from pathlib import Path from rbc.core.fileops import file_tmp_copy +from rbc.core.niwrap import generate_exec_folder __all__ = ["deoblique_and_reorient", "merge_3d_to_4d", "split_4d"] @@ -49,19 +50,24 @@ def deoblique_and_reorient( def split_4d(img_4d: Path) -> list[Path]: """Split a 4D NIfTI timeseries into individual 3D volumes. + Volumes are written as uncompressed NIfTI (.nii) to avoid gzip + overhead on float intermediates that are read back immediately. + Args: img_4d: Path to a 4D NIfTI image. Returns: Sorted list of paths to the individual 3D volume files. """ - split_result = fsl.fslsplit( - infile=img_4d, separation_time=True, output_basename="vol_" - ) - assert split_result.out_files is not None # noqa: S101 - out_files = split_result.out_files - out_dir = out_files[0].parent if isinstance(out_files, list) else out_files.parent - return sorted(out_dir.glob("vol_*.nii.gz")) + img = nib.nifti1.load(img_4d) + volumes = nib.four_to_three(img) + out_dir = generate_exec_folder(suffix="split4d") + paths: list[Path] = [] + for idx, vol in enumerate(volumes): + out_path = out_dir / f"vol_{idx:04d}.nii" + nib.save(vol, out_path) + paths.append(out_path) + return paths def merge_3d_to_4d(volumes: Sequence[Path], output: Path) -> Path: diff --git a/src/rbc/core/functional/distortion.py b/src/rbc/core/functional/distortion.py index f2529664..2e2c5a30 100644 --- a/src/rbc/core/functional/distortion.py +++ b/src/rbc/core/functional/distortion.py @@ -349,7 +349,7 @@ def correct_distortion_phasediff( diff_img = nib.Nifti1Image( diff_data, affine=p1_img.affine, header=p1_img.header ) - phasediff = out_dir / "phasediff.nii.gz" + phasediff = out_dir / "phasediff.nii" nib.save(diff_img, phasediff) # 4. Prepare fieldmap (rad/s) @@ -454,7 +454,7 @@ def correct_distortion_pepolar( # 2. Merge forward/reverse into 4D merged_path = merge_3d_to_4d( volumes=[epi_forward, epi_reverse], - output=out_dir / "merged_epi.nii.gz", + output=out_dir / "merged_epi.nii", ) # 3. Estimate field with TOPUP diff --git a/src/rbc/core/functional/masking.py b/src/rbc/core/functional/masking.py index 20e18421..825ce3c2 100644 --- a/src/rbc/core/functional/masking.py +++ b/src/rbc/core/functional/masking.py @@ -183,7 +183,7 @@ def bold_masking( warped_probseg = ants.ants_apply_transforms( reference_image=bold_ref, output=ants.ants_apply_transforms_warped_output( - warped_output_file_name="probseg_transform.nii.gz" + warped_output_file_name="probseg_transform.nii" ), default_value=0, float_=True, @@ -222,7 +222,7 @@ def bold_masking( # mismatches before N4 correction. bold_ref_dir_corrected = ants.set_direction_by_matrix( infile=bold_ref, - outfile="bold_ref_dir_corrected.nii.gz", + outfile="bold_ref_dir_corrected.nii", direction_matrix=parse_direction_matrix_from_header( dilated_binary_mask.output_file ), @@ -233,7 +233,7 @@ def bold_masking( n4_bias_correction = ants.n4_bias_field_correction( input_image=bold_ref_dir_corrected.outfile, output=ants.n4_bias_field_correction_corrected_output( - corrected_output_file_name="ref_bold_corrected.nii.gz" + corrected_output_file_name="ref_bold_corrected.nii" ), image_dimensionality=3, bspline_fitting=ants.n4_bias_field_correction_bspline_fitting( @@ -274,7 +274,7 @@ def bold_masking( fsl.fslmaths_operation_mas(mas=dilated_bet_mask.output_file), fsl.fslmaths_operation_seed(seed=seed), ], - output="ref_bold_corrected_brain_masked.nii.gz", + output="ref_bold_corrected_brain_masked.nii", ) # --- Phase 5: Intensity Uniformization & Second-Pass --- @@ -286,7 +286,7 @@ def bold_masking( in_file=masked_bold.output_file, cl_frac=0.2, rbt=[18.3, 65, 90], - prefix="uni.nii.gz", + prefix="uni.nii", t2=True, ) diff --git a/src/rbc/core/functional/motion.py b/src/rbc/core/functional/motion.py index 391887d6..31ecbaa2 100644 --- a/src/rbc/core/functional/motion.py +++ b/src/rbc/core/functional/motion.py @@ -80,7 +80,7 @@ def extract_motion_reference(in_file: Path) -> Path: header=ref_im.header, ) - temp_slice_file = generate_exec_folder(suffix="motion_ref_input") / "slice.nii.gz" + temp_slice_file = generate_exec_folder(suffix="motion_ref_input") / "slice.nii" ref_im.to_filename(temp_slice_file) mc_output_prefix = f"{_MC_PREFIX}_volreg.nii.gz" diff --git a/src/rbc/core/functional/resampling.py b/src/rbc/core/functional/resampling.py index 742a23bb..dc596542 100644 --- a/src/rbc/core/functional/resampling.py +++ b/src/rbc/core/functional/resampling.py @@ -99,7 +99,7 @@ def apply_motion_transforms( default_value=0, dimensionality=3, output=ants.ants_apply_transforms_warped_output( - f"vol_{idx:04d}_motion.nii.gz" + f"vol_{idx:04d}_motion.nii" ), ) transformed_vols.append(result.output.output_image_outfile) @@ -193,7 +193,7 @@ def resample_bold_to_template( default_value=0, dimensionality=3, output=ants.ants_apply_transforms_warped_output( - f"vol_{idx:04d}_template.nii.gz" + f"vol_{idx:04d}_template.nii" ), ) transformed_vols.append(result.output.output_image_outfile) diff --git a/src/rbc/core/longitudinal/transform.py b/src/rbc/core/longitudinal/transform.py index 37bf2514..1289ee34 100644 --- a/src/rbc/core/longitudinal/transform.py +++ b/src/rbc/core/longitudinal/transform.py @@ -137,7 +137,7 @@ def _transform_4d_chunked(in_file: Path, template: Path, xfm: Path) -> Path: dimensionality=3, interpolation=ants.ants_apply_transforms_linear(), output=ants.ants_apply_transforms_warped_output( - f"vol_{idx:04d}_template.nii.gz" + f"vol_{idx:04d}_template.nii" ), ) transformed_vols.append(result.output.output_image_outfile) From 9da1d30877eb8f91808dd5b230c878c343acf5eb Mon Sep 17 00:00:00 2001 From: Florian Rupprecht Date: Wed, 8 Apr 2026 23:19:47 -0400 Subject: [PATCH 2/2] Revert fslmaths output to .nii.gz (FSL ignores extension in Docker) FSL uses FSLOUTPUTTYPE to determine the output format, ignoring the filename extension. Keep fslmaths outputs as .nii.gz since the Docker container defaults to NIFTI_GZ. --- src/rbc/core/functional/masking.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/rbc/core/functional/masking.py b/src/rbc/core/functional/masking.py index 825ce3c2..e19e4a19 100644 --- a/src/rbc/core/functional/masking.py +++ b/src/rbc/core/functional/masking.py @@ -274,7 +274,7 @@ def bold_masking( fsl.fslmaths_operation_mas(mas=dilated_bet_mask.output_file), fsl.fslmaths_operation_seed(seed=seed), ], - output="ref_bold_corrected_brain_masked.nii", + output="ref_bold_corrected_brain_masked.nii.gz", ) # --- Phase 5: Intensity Uniformization & Second-Pass ---