ANTsX / ANTs

Advanced Normalization Tools (ANTs)
Apache License 2.0
1.21k stars 381 forks source link

Output sform matrix does not match that of the target image. #1804

Closed ckovach closed 1 week ago

ckovach commented 1 week ago

Operating system and version

Cent OS 7

CPU architecture

x86_64 (PC, Intel Mac, other Intel/AMD)

ANTs code version

ANTs Version: 2.1.0.post783-g64645 Compiled: Apr 25 2017 23:53:19

ANTs installation type

Compiled from source

Summary of the problem

I recently noticed that the sform matrix in the output NIFTI from antsRegistrationSyN.sh sometimes does not match that of the target volume, which creates some confusing discrepancies in the alignment of the images. We found this after being confounded by images that align well in voxel space but not upon being viewed in fsleyes, freeview, leadDBS, etc. I eventually traced the problem to cases in which the q_offset values in the target NIFTI are discrepant with the translation component of the sform matrix. ANTs evidently substitutes the sform matrix from the target image with one derived from qform values. I'd urge you you to preserve all the transformations from the target image as they are, so that images align regardless of the space in which a given viewer renders them.

Commands to reproduce the problem.

antsRegistrationSyN.sh -d 3 -n ${nthreads} -t b -o ${prefix} -f ${T1ind} -m ${T1tmp} 2>&1 > ${logfile}

Output of the command with verbose output.

N/A

Data to reproduce the problem

N/A

ntustison commented 1 week ago

Have seen this?

ckovach commented 1 week ago

Sorry - After posting this, I did find the explanation of how qform and sform transformations are handled here: https://github.com/ANTsX/ANTs/wiki/How-does-ANTs-handle-qform-and-sform-in-NIFTI-1-images%3F

This should not be an issue if I update to ANTs >= 2.3.5, correct?

cookpa commented 1 week ago

Hi,

This is handled upstream of ANTs in ITK.

There has been an ongoing debate about how best to handle NIFTI I/O. Because of ITK's limitations on the transforms it can use (rotation + offset + voxel scaling), there are a some compromises and tradeoffs that can affect some use cases.

I went through the current code and made a flow chart of the heuristic

https://github.com/InsightSoftwareConsortium/ITK/issues/4839#issuecomment-2383674232

This shows what is done on read. For writing images, the internal transform (rotation + offset + voxel scaling), however defined, is written to both the qform and sform, and both are set to NIFTI_XFORM_SCANNER_ANAT.

If you can post the headers of your fixed and moving images, I can probably tell how modern ANTs would handle them.

ckovach commented 1 week ago

And thanks for the speedy reply!

ckovach commented 1 week ago

Here is the header for the target image:

endian little
sizeof_hdr 348
data_type 0 0 0 0 0 0 0 0 0 0
db_name 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
extents 16384
session_error 0
regular 114
hkey_un0 0
dim0 3
dim1 256
dim2 216
dim3 170
dim4 1
dim5 1
dim6 1
dim7 1
vox_units 0 0 0 0
cal_units 0 0 0 0 0 0 0 0
unused1 0
datatype 16
bitpix 32
dim_un0 0
qfac 1
pixdim1 1
pixdim2 1
pixdim3 1
pixdim4 0.00516
pixdim5 0
pixdim6 0
pixdim7 0
vox_offset 352
funused1 1
funused2 0
funused3 6.162976e-33
cal_max 0
cal_min 0
compressed 0
verified 0
glmax 0
glmin 0
descrip 50 50 48 51 46 54 45 100 105 114 116 121 32 50 48 50 50 45 48 57 45 48 56 84 49 53 58 51 56 58 53 54 43 48 49 58 48 48 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 aux_file 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
qform_code 1
sform_code 1
quatern_b 0
quatern_c 0
quatern_d 0
qoffset_x -130.4428
qoffset_y -116.42
qoffset_z -113.4488
srow_x 1 0 0 -130.4428
srow_y 0 1 0 -116.42
srow_z 0 0 1 -71.44881
intent_name 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
magic 110 43 49 0

ckovach commented 1 week ago

And from the source:

endian little
sizeof_hdr 348
data_type 0 0 0 0 0 0 0 0 0 0
db_name 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
extents 0
session_error 0
regular 114
hkey_un0 0
dim0 3
dim1 260
dim2 311
dim3 260
dim4 1
dim5 1
dim6 1
dim7 1
vox_units 0 0 0 0
cal_units 0 0 0 0 0 0 0 0
unused1 0
datatype 64
bitpix 64
dim_un0 0
qfac -1
pixdim1 0.7
pixdim2 0.7
pixdim3 0.7
pixdim4 0
pixdim5 0
pixdim6 0
pixdim7 0
vox_offset 352
funused1 1
funused2 0
funused3 9.403955e-38
cal_max 0
cal_min 0
compressed 0
verified 0
glmax 0
glmin 0
descrip 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 aux_file 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
qform_code 2
sform_code 1
quatern_b -0
quatern_c 1
quatern_d 0
qoffset_x 90
qoffset_y -126
qoffset_z -72
srow_x -0.7 -0 0 90
srow_y -0 0.7 -0 -126
srow_z 0 0 0.7 -72
intent_name 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
magic 110 43 49 0

cookpa commented 1 week ago

Thanks, this looks like what ANTs 2.1 would produce, qform_code was set to 2, sform_code set to 1. The qform is always rigid so it was used on read, and then the sform would be set to the same transform.

Later versions set qform_code=1 and sform_code=0, to make clear that qform was being used. However, this made precision errors worse, so the switch was made to set qform_code=1 and sform_code=1, and use the sform where possible.

Looking at these, I think if you use a recent ANTs, the output should match the target header.

ckovach commented 1 week ago

Perfect- many thanks!

ckovach commented 1 week ago

In the interest of anyone who encounters the same problem, here's a quick follow up. The source of the sform and qform discrepancy in our case came from the "Crop 3D images" option in converting to nifti with [dcm2niix](https://www.nitrc.org/plugins/mwiki/index.php/dcm2nii:MainPage#:~:text=x-,Crop%20output,-%3A%20(%22%2Dx%20y). Without this options the qform and sform transforms are consistent.