Confused from attempts at converting composite h5 from fmriprep into fsl warp -- one way's fine but not the other?

Conveniently, fMRIPrep produces both a from-T1w_to-${template} and from-${template}_to-T1wcomposite transform. I need to convert these into a format for use by FSL. I’m decomposing these with ANTsand then converting them with wb_command. This works great in one direction, but not the other, and I’m not sure why. It seems like there’s a missing affine, but I don’t see where.

#!/bin/bash

sub=002
standard="MNI152_T1_1mm_brain_mask.nii.gz"
highres="sub-${sub}/anat/sub-${sub}_desc-preproc_t1w.nii.gz"

# this works

standard2highres_h5="sub-${sub}/anat/sub-${sub}_from-MNI152NLin6Asym_to-T1w_mode-image_xfm.h5"
standard2highres_prefix=from-MNI152NLin6Asym_to-T1w

CompositeTransformUtil \
	--disassemble $standard2highres_h5 \
	${standard2highres_prefix}

standard2highres_mat=standard2highres.mat
pixi run python write_affine.py $standard2highres_h5 $highres $standard $standard2highres_mat 1

standard2highres_warp=standard2highres_warp.nii.gz
wb_command \
	-convert-warpfield \
	-from-itk ${standard2highres_prefix}_00_DisplacementFieldTransform.nii.gz \
	-to-fnirt ${standard2highres_warp} ${standard}

applywarp \
	-i MNI152_T1_1mm.nii.gz \
	-o mni_highres.nii.gz \
	-r ${highres} \
	-w ${standard2highres_warp} \
	--postmat=${standard2highres_mat}

# check
# fsleyes mni_highres.nii.gz sub-${sub}/anat/sub-${sub}_desc-brain_mask.nii.gz

highres2standard_h5="sub-${sub}/anat/sub-${sub}_from-T1w_to-MNI152NLin6Asym_mode-image_xfm.h5"
highres2standard_mat=highres2standard.mat

highres2standard_prefix=from-T1w_to-MNI152NLin6Asym

# but this fails?
CompositeTransformUtil \
	--disassemble $highres2standard_h5 \
	${highres2standard_prefix}

pixi run python write_affine.py $highres2standard_h5 $standard $highres $highres2standard_mat 0

highres2standard_warp=highres2standard_warp.nii.gz
wb_command \
	-convert-warpfield \
	-from-itk ${highres2standard_prefix}_01_DisplacementFieldTransform.nii.gz \
	-to-fnirt ${highres2standard_warp} ${highres}

applywarp \
	-i ${highres} \
	-o mni.nii.gz \
	-r ${standard} \
	-w ${highres2standard_warp} \
	--premat=${highres2standard_mat}

# check
# fsleyes mni.nii.gz $standard

# also, this is fine?
antsApplyTransforms -t $highres2standard_h5 -r $standard -i $highres -o mni_ants.nii.gz

# check
# fsleyes mni_ants.nii.gz $standard

with the python file

import sys

from nitransforms import io


h5 = sys.argv[1]
reference = sys.argv[2]
moving = sys.argv[3]
mat = sys.argv[4]
transform = int(sys.argv[5])

xfm = io.itk.ITKCompositeH5.from_filename(h5)
io.fsl.FSLLinearTransform.from_ras(
    xfm[transform].to_ras(reference=reference, moving=moving),
    moving=moving,
    reference=reference,
).to_filename(mat)

and pixi environment

[workspace]
authors = ["Patrick Sadil <psadil@gmail.com>"]
channels = ["conda-forge"]
name = "tmp"
platforms = ["osx-arm64"]
version = "0.1.0"

[tasks]

[dependencies]
nitransforms = ">=24.1.4,<25"

First check (looking good)

second check (looking bad :frowning: )

third check (this looks fine, so it’s not the h5…)

I’m stuck trying to figure out what’s going wrong here and out of ideas – any help would be appreciated!

The one possibly weird thing that I’m noticing is that the wb_command has significantly expanded the range of voxel intensities for the failing warp, but I’m not sure what to make of that.

# okay
❯ fslstats from-MNI152NLin6Asym_to-T1w_00_DisplacementFieldTransform.nii.gz -R
-9.244793 6.800364 
# okay
❯ fslstats $standard2highres_warp -R 
-9.244797 7.281265 
# okay
❯ fslstats from-T1w_to-MNI152NLin6Asym_01_DisplacementFieldTransform.nii.gz -R
-6.807850 9.237727 
# different?
❯ fslstats $highres2standard_warp -R  
-25.169922 84.543442
❯ wb_command -version
Connectome Workbench
Type: Command Line Application
Version: 2.1.0
Qt Compiled Version: 6.8.0
Qt Runtime Version: 6.8.0
Commit: 99bf66b28572005f6419330d3f58b6262637412d
Commit Date: 2025-05-29 13:57:32 -0500
Compiler: c++ (/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin)
Compiler Version: 15.0.0.15000309
Compiled Debug: NO
Operating System: Apple OSX
Compiled with OpenMP: YES

❯ cd $ants_repo && git rev-parse HEAD
aaa34d6a53e6c3c415b51d376333cdf44ae315f5

Just a note: I can get that t1w→standard warp working when using FSL’s invwarp

invwarp -w $standard2highres_warp -o highres2standard_warp_fsl -r $standard

applywarp \
	-i ${highres} \
	-o mni_fsl.nii.gz \
	-r ${standard} \
	-w highres2standard_warp_fsl \
	--premat=${highres2standard_mat}

# fsleyes mni_fsl.nii.gz $standard

I’m asking about the wb_command approach mainly just to better understand the situation.

I wonder if this has to do with the reference image header transforms. I’m guessing this

standard="MNI152_T1_1mm_brain_mask.nii.gz"

is from FSL, whereas the fmriprep templates will be from templateflow. They have the same physical space but different qform and sform transforms.

Not sure – I’m getting similar results when setting the standard to a file from templateflow

I think I fixed it by changing

wb_command \
	-convert-warpfield \
	-from-itk ${highres2standard_prefix}_01_DisplacementFieldTransform.nii.gz \
	-to-fnirt ${highres2standard_warp} ${highres}

to

wb_command \
	-convert-warpfield \
	-from-itk ${highres2standard_prefix}_01_DisplacementFieldTransform.nii.gz \
	-to-fnirt ${highres2standard_warp} ${standard}

I think this works because ${standard} is the “fixed” space in the ANTs registration, and the displacement fields for both the forward and inverse warps are both written in the fixed space. They are composed with the affines in a different order, which is handled by the pre / post mat args to applywarp.

Oh! Indeed, this seems to work for me. I hadn’t realized that the “fixed” space was considered the same in each of those files.