Convert FreeSurfer Transform to ANTs H5 Transform (with proper header)

Hi,

I am looking into replacing an ANTs registration step with FreeSurfer SynthMorph for a processing pipeline. FreeSurfer uses RAS orientation while ANTs/ITK uses LPS.

I can successfully get my RAS xfm to LPS with FreeSurfer’s mri_warp_convert -g moving.nii.gz --inras warp.mgz --outlps warp.h5, but that resultant .H5 file does not have the expected ANTs H5 header, which leads me to the following error when the registration is applied later with antsApplyTransform:

	Description: ITK ERROR: TransformFileReaderTemplate(0x55565df6abb0): Could not create Transform IO object for reading file /home/smeisler/projects/fmriprep_ants/single_subject_bids/derivatives/synthmorph_test/sub-NDARINV1Y40DZT8/ses-baselineYear1Arm1/anat/sub-NDARINV1Y40DZT8_ses-baselineYear1Arm1_rec-normalized_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5
	  Tried to create one of the following:
	    HDF5TransformIOTemplate
	    HDF5TransformIOTemplate
	    MINCTransformIOTemplate
	    MINCTransformIOTemplate
	    MatlabTransformIOTemplate
	    MatlabTransformIOTemplate
	    TxtTransformIOTemplate
	    TxtTransformIOTemplate
	  You probably failed to set a file suffix, or
	    set the suffix to an unsupported type.

Unfortunately, I need to keep the xfm to be an .h5 file, or at least I would like to (otherwise I’ll have to tinker with more of the pipeline than I had anticipated).

So the main question is: Is there a utility that can add ANTs header information to a transform file such that antsApplyRegistration will behave better?

Thanks,
Steven

As an update, I have tried the following to manually convert,

   def convert_nifti_to_ants_h5_deformation(self, nifti_file, h5_file):
        # Load the NIfTI file
        img = nib.load(nifti_file)
        data = img.get_fdata()

        # Ensure the data is in float32 format for compatibility
        if data.dtype != np.float32:
            data = data.astype(np.float32)

        # Create the HDF5 file with ANTs specific structure
        with h5py.File(h5_file, 'w') as f:
            # Create the Transform group
            xform_group = f.create_group('/TransformGroup/0')
            
            # Write out the transformation parameters
            xform_group.create_dataset('TransformParameters', data=data.flatten(order='C'))
            
            # Write additional attributes and datasets expected by ANTs
            xform_group.attrs['TransformType'] = 'DisplacementFieldTransform'
            xform_group.attrs['TransformDisplacementField'] = 'DisplacementField'
            xform_group.attrs['TransformFixedParameters'] = img.affine.flatten().tolist()
            
            f.attrs['ITKTransformType'] = 'DisplacementFieldTransform_double_3_3'
            f.attrs['ITKVersion'] = '4.11.0'
            f.attrs['HDFVersion'] = '1.8.15'

and now ANTs returns the following errors when I go to apply the registration:

Stderr:
	HDF5-DIAG: Error detected in HDF5 (1.14.3) thread 1:
	  #000: H5D.c line 403 in H5Dopen2(): unable to synchronously open dataset
	    major: Dataset
	    minor: Can't open object
	  #001: H5D.c line 364 in H5D__open_api_common(): unable to open dataset
	    major: Dataset
	    minor: Can't open object
	  #002: H5VLcallback.c line 1980 in H5VL_dataset_open(): dataset open failed
	    major: Virtual Object Layer
	    minor: Can't open object
	  #003: H5VLcallback.c line 1947 in H5VL__dataset_open(): dataset open failed
	    major: Virtual Object Layer
	    minor: Can't open object
	  #004: H5VLnative_dataset.c line 331 in H5VL__native_dataset_open(): unable to open dataset
	    major: Dataset
	    minor: Can't open object
	  #005: H5Dint.c line 1418 in H5D__open_name(): not found
	    major: Dataset
	    minor: Object not found
	  #006: H5Gloc.c line 421 in H5G_loc_find(): can't find object
	    major: Symbol table
	    minor: Object not found
	  #007: H5Gtraverse.c line 816 in H5G_traverse(): internal path traversal failed
	    major: Symbol table
	    minor: Object not found
	  #008: H5Gtraverse.c line 596 in H5G__traverse_real(): traversal operator failed
	    major: Symbol table
	    minor: Callback failed
	  #009: H5Gloc.c line 381 in H5G__loc_find_cb(): object 'TransformType' doesn't exist
	    major: Symbol table
	    minor: Object not found
	Transform reader for /home/smeisler/projects/fmriprep_ants/single_subject_bids/derivatives/synthmorph_test/sub-NDARINV1Y40DZT8/ses-baselineYear1Arm1/anat/sub-NDARINV1Y40DZT8_ses-baselineYear1Arm1_rec-normalized_from-T1w_to-MNI152NLin2009cAsym_mode-image_xfm.h5 caught an ITK exception:

	itk::ExceptionObject (0x560251e0ed50)
	Location: "unknown" 
	File: /home/conda/feedstock_root/build_artifacts/libitk_1717078286681/work/Modules/IO/TransformHDF5/src/itkHDF5TransformIO.cxx
	Line: 365
	Description: ITK ERROR: HDF5TransformIOTemplate(0x560251d81890): H5Dopen2 failed

Is this manual setting a good direction to go down or are there more automated tools for this?

Thanks,
Steven

Another update, working with the ITK support group seems to have provided a good solution: Convert .nii.gz warp to .h5 (with proper header) - #4 by smeisler - Beginner Questions - ITK

I would like to share my experience with this issue.
I was trying to convert the NIfTI warp generated by SynthMorph (in FreeSurfer 7-dev) into an ANTs-compatible .h5 transform file.

I initially followed the approach described by Dr. Meisler (Convert .nii.gz warp to .h5 (with proper header) - #5 by blowekamp - Beginner Questions - ITK), but encountered dimension mismatch errors, which were also discussed here.
(I am not sure whether this issue still exists in the SynthMorph version bundled with FreeSurfer 8.)

Eventually, the following solution worked for me:

import nibabel as nib
import numpy as np
import SimpleITK as sitk
import os
import tempfile

def nifti_warp_to_h5(input_nifti: str, output_h5: str):
    """
    Convert a 4D NIfTI warp field (x,y,z,3) to an ANTs-compatible .h5 transform file.
    The function creates a temporary NIFTI_INTENT_DISPVECT displacement field,
    which is removed after the .h5 is written.
    """

    print(f"Loading warp: {input_nifti}")
    warped = nib.load(input_nifti)
    data = warped.get_fdata()
    affine = warped.affine

    shape = data.shape
    if len(shape) != 4 or shape[3] != 3:
        raise ValueError("Input image must be 4D with last dimension size 3 (x,y,z,3).")

    # Reshape to (X, Y, Z, 1, 3)
    data = np.reshape(data, (shape[0], shape[1], shape[2], 1, 3))

    # Convert voxel displacements to physical displacements
    reshaped = np.reshape(data, (3, -1))
    stacked = np.vstack([reshaped, np.zeros(reshaped.shape[1])])
    multiplied = np.matmul(affine, stacked)[:3,]
    last = np.reshape(multiplied, data.shape)

    # Save temporary displacement field with DISPVECT intent
    temp_disp = tempfile.NamedTemporaryFile(suffix="_displacement.nii.gz", delete=False).name
    new_img = nib.Nifti1Image(last, affine)
    new_img.header.set_intent(nib.nifti1.intent_codes['NIFTI_INTENT_DISPVECT'])
    nib.save(new_img, temp_disp)
    print(f"Saved temporary displacement field: {temp_disp}")

    # Convert to ANTs-compatible HDF5 transform
    print("Converting to ANTs HDF5 transform...")
    displacement_image = sitk.ReadImage(
        temp_disp,
        sitk.sitkVectorFloat64,
        imageIO="NiftiImageIO"
    )
    tx = sitk.DisplacementFieldTransform(displacement_image)
    sitk.WriteTransform(tx, output_h5)
    print(f"Saved ANTs-compatible transform: {output_h5}")

    # Remove temporary file
    try:
        os.remove(temp_disp)
        print("Temporary file removed.")
    except Exception as e:
        print(f"Warning: failed to remove temporary file {temp_disp}: {e}")

Although antsTransformInfo may still fail to read the resulting .h5, the transform works correctly with antsApplyTransforms, which is sufficient for most downstream processing.