Skip to content

Commit 40e3770

Browse files
authored
feat: Replace c3d_affine_tool with a ConvertAffine interface (#3464)
Upgrading convert3d leads to segfaults, which means that we are playing a game of cat and mouse with ANTs, libitk and convert3d. It's not worth it just to convert FSL affines to ITK, when we can do this with nitransforms. The existing niworkflows `ConcatenateXFMs` interface does not quite do the job, since it assumes that `.mat` is an ITK binary affine, so I create a `ConvertAffine` interface. This replaces the `fsl2itk_fwd`, `fsl2itk_inv` and `invt_bbr` nodes, and moves the `lta_to_fsl` node to the section it is connected in. This PR could go further and stop using `LTAConvert` as well, but I intend this to be a narrow replacement. The main logical difference is that I only convert the `mri_coreg` output to FSL format if FLIRT is going to run; otherwise, I convert its output directly to ITK to avoid converting twice.
2 parents f3464b5 + a254756 commit 40e3770

16 files changed

+260
-64
lines changed

REFERENCES.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
| nibabel | https://doi.org/10.5281/zenodo.60808 | https://github.com/nipy/nibabel/ |
3434
| nilearn | https://doi.org/10.3389/fninf.2014.00014 | https://github.com/nilearn/nilearn/ |
3535
| nipype | https://doi.org/10.3389/fninf.2011.00013 https://doi.org/10.5281/zenodo.581704 | https://github.com/nipy/nipype/ |
36-
| convert3d | | https://sourceforge.net/projects/c3d/ |
3736
| **Graphics**
3837
| seaborn | https://doi.org/10.5281/zenodo.883859 | https://github.com/mwaskom/seaborn |
3938
| matplotlib 2.0.0 | https://doi.org/10.5281/zenodo.248351 | https://github.com/matplotlib/matplotlib |

docs/installation.rst

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,6 @@ the ``fmriprep`` package:
8888
- FSL_ (version 6.0.7.7)
8989
- ANTs_ (version 2.5.1)
9090
- AFNI_ (version 24.0.05)
91-
- `C3D <https://sourceforge.net/projects/c3d/>`_ (version 1.4.0)
9291
- FreeSurfer_ (version 7.3.2)
9392
- `bids-validator <https://github.com/bids-standard/bids-validator>`_ (version 1.14.0)
9493
- `connectome-workbench <https://www.humanconnectome.org/software/connectome-workbench>`_ (version 1.5.0)

env.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ dependencies:
3131
# Try to remove this ASAP
3232
# https://github.com/conda-forge/ants-feedstock/issues/19
3333
- libitk=5.4.0
34-
# Workflow dependencies: Convert3d
35-
- convert3d=1.4
3634
# Workflow dependencies: Connectome Workbench
3735
- connectome-workbench-cli=2.0
3836
# Workflow dependencies: FSL (versions pinned in 6.0.7.13)

fmriprep/interfaces/conftest.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
from pathlib import Path
21
from shutil import copytree
32

43
import pytest
54

5+
from .tests.data import load
6+
67
try:
78
from contextlib import chdir as _chdir
89
except ImportError: # PY310
@@ -19,17 +20,19 @@ def _chdir(path):
1920
os.chdir(cwd)
2021

2122

22-
@pytest.fixture(scope='module')
23+
@pytest.fixture
2324
def data_dir():
24-
return Path(__file__).parent / 'tests' / 'data'
25+
with load.as_path() as data_dir:
26+
yield data_dir
2527

2628

2729
@pytest.fixture(autouse=True)
2830
def _docdir(request, tmp_path):
2931
# Trigger ONLY for the doctests.
3032
doctest_plugin = request.config.pluginmanager.getplugin('doctest')
3133
if isinstance(request.node, doctest_plugin.DoctestItem):
32-
copytree(Path(__file__).parent / 'tests' / 'data', tmp_path, dirs_exist_ok=True)
34+
with load.as_path() as data_dir:
35+
copytree(data_dir, tmp_path, dirs_exist_ok=True)
3336

3437
# Chdir only for the duration of the test.
3538
with _chdir(tmp_path):

fmriprep/interfaces/nitransforms.py

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*-
2+
# vi: set ft=python sts=4 ts=4 sw=4 et:
3+
#
4+
# Copyright 2025 The NiPreps Developers <[email protected]>
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
# We support and encourage derived works from this project, please read
19+
# about our expectations at
20+
#
21+
# https://www.nipreps.org/community/licensing/
22+
#
23+
"""Wrappers of NiTransforms."""
24+
25+
from pathlib import Path
26+
27+
from nipype.interfaces.base import (
28+
BaseInterfaceInputSpec,
29+
File,
30+
SimpleInterface,
31+
TraitedSpec,
32+
traits,
33+
)
34+
from nipype.utils.filemanip import fname_presuffix
35+
36+
37+
class _ConvertAffineInputSpec(BaseInterfaceInputSpec):
38+
in_xfm = File(exists=True, desc='input transform piles')
39+
inverse = traits.Bool(False, usedefault=True, desc='generate inverse')
40+
in_fmt = traits.Enum('auto', 'itk', 'fs', 'fsl', usedefault=True, desc='input format')
41+
out_fmt = traits.Enum('itk', 'fs', 'fsl', usedefault=True, desc='output format')
42+
reference = File(exists=True, desc='reference file')
43+
moving = File(exists=True, desc='moving file')
44+
45+
46+
class _ConvertAffineOutputSpec(TraitedSpec):
47+
out_xfm = File(exists=True, desc='output, combined transform')
48+
out_inv = File(desc='output, combined transform')
49+
50+
51+
class ConvertAffine(SimpleInterface):
52+
"""Write a single, flattened transform file."""
53+
54+
input_spec = _ConvertAffineInputSpec
55+
output_spec = _ConvertAffineOutputSpec
56+
57+
def _run_interface(self, runtime):
58+
from nitransforms.linear import load as load_affine
59+
60+
ext = {
61+
'fs': 'lta',
62+
'itk': 'txt',
63+
'fsl': 'mat',
64+
}[self.inputs.out_fmt]
65+
66+
in_fmt = self.inputs.in_fmt
67+
if in_fmt == 'auto':
68+
in_fmt = {
69+
'.lta': 'fs',
70+
'.mat': 'fsl',
71+
'.txt': 'itk',
72+
}[Path(self.inputs.in_xfm).suffix]
73+
74+
reference = self.inputs.reference or None
75+
moving = self.inputs.moving or None
76+
affine = load_affine(self.inputs.in_xfm, fmt=in_fmt, reference=reference, moving=moving)
77+
78+
out_file = fname_presuffix(
79+
self.inputs.in_xfm,
80+
suffix=f'_fwd.{ext}',
81+
newpath=runtime.cwd,
82+
use_ext=False,
83+
)
84+
self._results['out_xfm'] = out_file
85+
affine.to_filename(out_file, moving=moving, fmt=self.inputs.out_fmt)
86+
87+
if self.inputs.inverse:
88+
inv_affine = ~affine
89+
if moving is not None:
90+
inv_affine.reference = moving
91+
92+
out_inv = fname_presuffix(
93+
self.inputs.in_xfm,
94+
suffix=f'_inv.{ext}',
95+
newpath=runtime.cwd,
96+
use_ext=False,
97+
)
98+
self._results['out_inv'] = out_inv
99+
inv_affine.to_filename(out_inv, moving=reference, fmt=self.inputs.out_fmt)
100+
101+
return runtime
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from acres import Loader
2+
3+
load = Loader(__spec__.name)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: MatrixOffsetTransformBase_double_3_3
4+
Parameters: 0.9999895968880486 -0.004093655352697662 -0.0020605942003409524 0.004033765542180662 0.9995917628463604 -0.028289559719570527 0.0021755887788865217 0.028280980403387748 0.9995979209696005 -0.31051619074750103 -0.7081012785638218 0.4403933958178312
5+
FixedParameters: 0 0 0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: MatrixOffsetTransformBase_double_3_3
4+
Parameters: 0.9999893987506181 0.0040337317156719996 0.0021755595966243754 -0.00409368802815685 0.999591499554368 0.02828093916460761 -0.0020606207364131553 -0.028289577997839488 0.9995973713325053 0.3124110873946151 0.6940861236277449 -0.4608878232636471
5+
FixedParameters: 0 0 0
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#Insight Transform File V1.0
2+
#Transform 0
3+
Transform: AffineTransform_double_3_3
4+
Parameters: 0.99998968839645386 -0.0040936507284641266 -0.0020606310572475195 0.0040338016115128994 0.99959170818328857 -0.028289616107940674 0.0021755544003099203 0.028280943632125854 0.99959772825241089 -0.31053543090820312 -0.70811450481414795 0.440399169921875
5+
FixedParameters: 0 0 0
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
0.99994755 0.00549713 -0.00865372 -5.78189802
2+
-0.00323471 0.97014636 0.24249923 -15.33529091
3+
0.00972842 -0.24245842 0.97011328 103.01686096
4+
0.00000000 0.00000000 0.00000000 1.00000000

0 commit comments

Comments
 (0)