Skip to content

Commit 92a2f65

Browse files
authored
Merge pull request #815 from effigies/fix/cifti_voxel_ordering
FIX: Generate CIFTI volume structure indices in column-major order
2 parents 83bb10e + 4f38b11 commit 92a2f65

File tree

2 files changed

+47
-18
lines changed

2 files changed

+47
-18
lines changed

niworkflows/interfaces/cifti.py

Lines changed: 14 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -339,30 +339,27 @@ def _create_cifti_image(
339339
else:
340340
model_type = "CIFTI_MODEL_TYPE_VOXELS"
341341
vox = []
342-
ts = None
342+
ts = []
343343
for label in labels:
344-
ijk = np.nonzero(label_data == label)
345-
if ijk[0].size == 0: # skip label if nothing matches
344+
# nonzero returns indices in row-major (C) order
345+
# NIfTI uses column-major (Fortran) order, so HCP generates indices in F order
346+
# Therefore flip the data and label the indices backwards
347+
k, j, i = np.nonzero(label_data.T == label)
348+
if k.size == 0: # skip label if nothing matches
346349
continue
347-
ts = (
348-
bold_data[ijk]
349-
if ts is None
350-
else np.concatenate((ts, bold_data[ijk]))
351-
)
352-
vox += [
353-
[ijk[0][idx], ijk[1][idx], ijk[2][idx]] for idx in range(len(ts))
354-
]
355-
356-
vox = ci.Cifti2VoxelIndicesIJK(vox)
350+
ts.append(bold_data[i, j, k])
351+
vox.append(np.stack([i, j, k]).T)
352+
353+
vox_indices_ijk = ci.Cifti2VoxelIndicesIJK(np.concatenate(vox))
357354
bm = ci.Cifti2BrainModel(
358355
index_offset=idx_offset,
359-
index_count=len(vox),
356+
index_count=len(vox_indices_ijk),
360357
model_type=model_type,
361358
brain_structure=structure,
362-
voxel_indices_ijk=vox,
359+
voxel_indices_ijk=vox_indices_ijk,
363360
)
364-
idx_offset += len(vox)
365-
bm_ts = np.column_stack((bm_ts, ts.T))
361+
idx_offset += len(vox_indices_ijk)
362+
bm_ts = np.column_stack((bm_ts, np.concatenate(ts).T))
366363
# add each brain structure to list
367364
brainmodels.append(bm)
368365

niworkflows/interfaces/tests/test_cifti.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import json
22
from pathlib import Path
3+
from unittest import mock
34

45
import nibabel as nb
56
import numpy as np
67
import pytest
78

8-
from ..cifti import GenerateCifti, CIFTI_STRUCT_WITH_LABELS
9+
from ..cifti import GenerateCifti, CIFTI_STRUCT_WITH_LABELS, _create_cifti_image
910

1011

1112
@pytest.fixture(scope="module")
@@ -56,3 +57,34 @@ def test_GenerateCifti(tmpdir, cifti_data):
5657
assert 'SpatialReference' in metadata
5758
for key in ('VolumeReference', 'CIFTI_STRUCTURE_LEFT_CORTEX', 'CIFTI_STRUCTURE_RIGHT_CORTEX'):
5859
assert key in metadata['SpatialReference']
60+
61+
62+
def test__create_cifti_image(tmp_path):
63+
bold_data = np.arange(8, dtype='f4').reshape((2, 2, 2, 1), order='F')
64+
LAS = [[-1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
65+
bold_img = nb.Nifti1Image(bold_data, LAS)
66+
label_img = nb.Nifti1Image(np.full((2, 2, 2), 16, 'u1'), LAS)
67+
68+
bold_file = tmp_path / 'bold.nii'
69+
volume_label = tmp_path / 'label.nii'
70+
bold_img.to_filename(bold_file)
71+
label_img.to_filename(volume_label)
72+
73+
# Only add one structure to the CIFTI file
74+
with mock.patch(
75+
'niworkflows.interfaces.cifti.CIFTI_STRUCT_WITH_LABELS',
76+
{'CIFTI_STRUCTURE_BRAIN_STEM': (16,)},
77+
):
78+
dummy_fnames = ('', '')
79+
cifti_file = _create_cifti_image(bold_file, volume_label, dummy_fnames, dummy_fnames, 2.0)
80+
81+
cimg = nb.load(cifti_file)
82+
series, bm = [cimg.header.get_axis(i) for i in (0, 1)]
83+
assert len(series) == 1 # Time
84+
assert len(bm) == 8 # Voxel
85+
86+
# Maintaining Fortran ordering, data comes out as it went in
87+
assert np.array_equal(cimg.get_fdata(), bold_data.reshape((1, 8), order='F'))
88+
89+
# Brain model voxels are indexed in Fortran order (fastest first)
90+
assert np.array_equal(bm.voxel[:4], [[0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0]])

0 commit comments

Comments
 (0)