Skip to content

Commit 7f47d73

Browse files
authored
Merge pull request #487 from effigies/calculate-transform-parameters
feat(syn): Update totalFieldVarianceInVoxel space based on voxel resolution
2 parents a8fb727 + 8bc08c0 commit 7f47d73

File tree

4 files changed

+147
-11
lines changed

4 files changed

+147
-11
lines changed

sdcflows/data/sd_syn.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"shrink_factors": [ [ 1, 1 ], [ 1 ] ],
1515
"sigma_units": [ "vox", "vox" ],
1616
"smoothing_sigmas": [ [ 2, 0 ], [ 0 ] ],
17-
"transform_parameters": [ [ 0.8, 6.0, 3.0 ], [ 0.8, 2.0, 1.0 ] ],
17+
"transform_parameters": [ [ 0.8, 6.0, 10.0 ], [ 0.8, 2.0, 0.5 ] ],
1818
"transforms": [ "SyN", "SyN" ],
1919
"use_histogram_matching": [ true, true ],
2020
"winsorize_lower_quantile": 0.001,

sdcflows/data/sd_syn_sloppy.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"shrink_factors": [ [ 1, 1 ], [ 1 ] ],
1515
"sigma_units": [ "vox", "vox" ],
1616
"smoothing_sigmas": [ [ 2, 0 ], [ 0 ] ],
17-
"transform_parameters": [ [ 0.8, 6.0, 3.0 ], [ 0.8, 2.0, 1.0 ] ],
17+
"transform_parameters": [ [ 0.8, 6.0, 10.0 ], [ 0.8, 2.0, 0.5 ] ],
1818
"transforms": [ "SyN", "SyN" ],
1919
"use_histogram_matching": [ true, true ],
2020
"verbose": true,

sdcflows/workflows/fit/syn.py

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"""
2424
Estimating the susceptibility distortions without fieldmaps.
2525
"""
26+
import json
2627

2728
from nipype.pipeline import engine as pe
2829
from nipype.interfaces import utility as niu
@@ -214,9 +215,14 @@ def init_syn_sdc_wf(
214215
find_zooms = pe.Node(niu.Function(function=_adjust_zooms), name="find_zooms")
215216
zooms_epi = pe.Node(RegridToZooms(), name="zooms_epi")
216217

218+
syn_config = data.load(f"sd_syn{'_sloppy' * sloppy}.json")
219+
220+
vox_params = pe.Node(niu.Function(function=_mm2vox), name="vox_params")
221+
vox_params.inputs.registration_config = json.loads(syn_config.read_text())
222+
217223
# SyN Registration Core
218224
syn = pe.Node(
219-
Registration(from_file=data.load(f"sd_syn{'_sloppy' * sloppy}.json")),
225+
Registration(from_file=syn_config),
220226
name="syn",
221227
n_procs=omp_nthreads,
222228
)
@@ -277,6 +283,7 @@ def init_syn_sdc_wf(
277283
("sd_prior", "in2")]),
278284
(inputnode, anat_dilmsk, [("anat_mask", "in_file")]),
279285
(inputnode, warp_dir, [("anat_ref", "fixed_image")]),
286+
(inputnode, vox_params, [("anat_ref", "fixed_image")]),
280287
(inputnode, anat_merge, [("anat_ref", "in1")]),
281288
(inputnode, lap_anat, [("anat_ref", "op1")]),
282289
(inputnode, find_zooms, [("anat_ref", "in_anat"),
@@ -295,11 +302,15 @@ def init_syn_sdc_wf(
295302
(anat_dilmsk, amask2epi, [("out_file", "input_image")]),
296303
(amask2epi, epi_umask, [("output_image", "in2")]),
297304
(readout_time, warp_dir, [("pe_direction", "pe_dir")]),
305+
(readout_time, vox_params, [("pe_direction", "pe_dir")]),
306+
(clip_epi, warp_dir, [("out_file", "moving_image")]),
307+
(clip_epi, vox_params, [("out_file", "moving_image")]),
298308
(warp_dir, syn, [("out", "restrict_deformation")]),
299309
(anat_merge, syn, [("out", "fixed_image")]),
300310
(fixed_masks, syn, [("out", "fixed_image_masks")]),
301311
(epi_merge, syn, [("out", "moving_image")]),
302312
(moving_masks, syn, [("out", "moving_image_masks")]),
313+
(vox_params, syn, [("out", "transform_parameters")]),
303314
(syn, extract_field, [(("forward_transforms", _pop), "transform")]),
304315
(clip_epi, extract_field, [("out_file", "epi")]),
305316
(readout_time, extract_field, [("readout_time", "ro_time"),
@@ -583,25 +594,52 @@ def _remove_first_mask(in_file):
583594
return workflow
584595

585596

586-
def _warp_dir(fixed_image, pe_dir, nlevels=3):
597+
def _warp_dir(moving_image, fixed_image, pe_dir, nlevels=2):
587598
"""Extract the ``restrict_deformation`` argument from metadata."""
588599
import numpy as np
589600
import nibabel as nb
590601

591-
img = nb.load(fixed_image)
602+
moving = nb.load(moving_image)
603+
fixed = nb.load(fixed_image)
592604

593-
if np.any(nb.affines.obliquity(img.affine) > 0.05):
605+
if np.any(nb.affines.obliquity(fixed.affine) > 0.05):
594606
from nipype import logging
595607

596608
logging.getLogger("nipype.interface").warn(
597609
"Running fieldmap-less registration on an oblique dataset"
598610
)
599611

600-
vs = nb.affines.voxel_sizes(img.affine)
601-
order = np.around(np.abs(img.affine[:3, :3] / vs))
602-
retval = order @ [1 if pe_dir[0] == ax else 0.1 for ax in "ijk"]
612+
moving_axcodes = nb.aff2axcodes(moving.affine, ["RR", "AA", "SS"])
613+
moving_pe_axis = moving_axcodes["ijk".index(pe_dir[0])]
614+
615+
fixed_axcodes = nb.aff2axcodes(fixed.affine, ["RR", "AA", "SS"])
616+
617+
deformation = [0.1, 0.1, 0.1]
618+
deformation[fixed_axcodes.index(moving_pe_axis)] = 1.0
619+
620+
return nlevels * [deformation]
621+
622+
623+
def _mm2vox(moving_image, fixed_image, pe_dir, registration_config):
624+
import nibabel as nb
625+
626+
params = registration_config['transform_parameters']
627+
628+
moving = nb.load(moving_image)
629+
# Use duplicate axcodes to ignore sign
630+
moving_axcodes = nb.aff2axcodes(moving.affine, ["RR", "AA", "SS"])
631+
moving_pe_axis = moving_axcodes["ijk".index(pe_dir[0])]
632+
633+
fixed = nb.load(fixed_image)
634+
fixed_axcodes = nb.aff2axcodes(fixed.affine, ["RR", "AA", "SS"])
635+
636+
zooms = nb.affines.voxel_sizes(fixed.affine)
637+
pe_res = zooms[fixed_axcodes.index(moving_pe_axis)]
603638

604-
return nlevels * [retval.tolist()]
639+
return [
640+
[*level_params[:2], level_params[2] / pe_res]
641+
for level_params in params
642+
]
605643

606644

607645
def _merge_meta(epi_ref, meta_list):

sdcflows/workflows/fit/tests/test_syn.py

Lines changed: 99 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,21 @@
2323
"""Test fieldmap-less SDC-SyN."""
2424

2525
import json
26+
27+
import numpy as np
28+
import nibabel as nb
2629
import pytest
2730
from nipype.pipeline import engine as pe
2831

29-
from ..syn import init_syn_sdc_wf, init_syn_preprocessing_wf, _adjust_zooms, _set_dtype
32+
from .... import data
33+
from ..syn import (
34+
init_syn_sdc_wf,
35+
init_syn_preprocessing_wf,
36+
_adjust_zooms,
37+
_set_dtype,
38+
_mm2vox,
39+
_warp_dir,
40+
)
3041

3142

3243
@pytest.mark.veryslow
@@ -254,3 +265,90 @@ def test_ensure_dtype(in_dtype, out_dtype, tmpdir):
254265
assert out_file == f"{in_dtype}.nii.gz"
255266
else:
256267
assert out_file == f"{in_dtype}_{out_dtype}.nii.gz"
268+
269+
270+
def axcodes2aff(axcodes):
271+
"""Return an affine matrix from axis codes."""
272+
return nb.orientations.inv_ornt_aff(
273+
nb.orientations.ornt_transform(
274+
nb.orientations.axcodes2ornt("RAS"),
275+
nb.orientations.axcodes2ornt(axcodes),
276+
),
277+
(10, 10, 10),
278+
)
279+
280+
281+
@pytest.mark.parametrize(
282+
("fixed_ornt", "moving_ornt", "ijk", "index"),
283+
[
284+
("RAS", "RAS", "i", 0),
285+
("RAS", "RAS", "j", 1),
286+
("RAS", "RAS", "k", 2),
287+
("RAS", "PSL", "i", 1),
288+
("RAS", "PSL", "j", 2),
289+
("RAS", "PSL", "k", 0),
290+
("PSL", "RAS", "i", 2),
291+
("PSL", "RAS", "j", 0),
292+
("PSL", "RAS", "k", 1),
293+
],
294+
)
295+
def test_mm2vox(tmp_path, fixed_ornt, moving_ornt, ijk, index):
296+
fixed_path = tmp_path / "fixed.nii.gz"
297+
moving_path = tmp_path / "moving.nii.gz"
298+
299+
# Use separate zooms to make identifying the conversion easier
300+
fixed_aff = np.diag((2, 3, 4, 1))
301+
nb.save(
302+
nb.Nifti1Image(np.zeros((10, 10, 10)), axcodes2aff(fixed_ornt) @ fixed_aff),
303+
fixed_path,
304+
)
305+
nb.save(
306+
nb.Nifti1Image(np.zeros((10, 10, 10)), axcodes2aff(moving_ornt)),
307+
moving_path,
308+
)
309+
310+
config = json.loads(data.load.readable("sd_syn.json").read_text())
311+
312+
params = config["transform_parameters"]
313+
mm_values = np.array([level[2] for level in params])
314+
315+
vox_params = _mm2vox(str(moving_path), str(fixed_path), ijk, config)
316+
vox_values = [level[2] for level in vox_params]
317+
assert [
318+
mm_level[:2] == vox_level[:2] for mm_level, vox_level in zip(params, vox_params)
319+
]
320+
assert np.array_equal(vox_values, mm_values / [2, 3, 4][index])
321+
322+
323+
@pytest.mark.parametrize(
324+
("fixed_ornt", "moving_ornt", "ijk", "index"),
325+
[
326+
("RAS", "RAS", "i", 0),
327+
("RAS", "RAS", "j", 1),
328+
("RAS", "RAS", "k", 2),
329+
("RAS", "PSL", "i", 1),
330+
("RAS", "PSL", "j", 2),
331+
("RAS", "PSL", "k", 0),
332+
("PSL", "RAS", "i", 2),
333+
("PSL", "RAS", "j", 0),
334+
("PSL", "RAS", "k", 1),
335+
],
336+
)
337+
def test_warp_dir(tmp_path, fixed_ornt, moving_ornt, ijk, index):
338+
fixed_path = tmp_path / "fixed.nii.gz"
339+
moving_path = tmp_path / "moving.nii.gz"
340+
341+
nb.save(
342+
nb.Nifti1Image(np.zeros((10, 10, 10)), axcodes2aff(fixed_ornt)),
343+
fixed_path,
344+
)
345+
nb.save(
346+
nb.Nifti1Image(np.zeros((10, 10, 10)), axcodes2aff(moving_ornt)),
347+
moving_path,
348+
)
349+
350+
for nlevels in range(1, 3):
351+
deformations = _warp_dir(str(moving_path), str(fixed_path), ijk, nlevels)
352+
assert len(deformations) == nlevels
353+
for val in deformations:
354+
assert val == [1.0 if i == index else 0.1 for i in range(3)]

0 commit comments

Comments
 (0)