function test_suite = test_fmri_io
% tests for fmri input/output
%
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
try % assignment of 'localfunctions' is necessary in Matlab >= 2016
test_functions = localfunctions();
catch % no problem; early Matlab versions can use initTestSuite fine
end
initTestSuite;
function test_fmri_io_nii_gz()
if ~usejava('jvm') || cosmo_wtf('is_octave')
cosmo_notify_test_skipped('java VM not available for .nii.gz');
return
end
save_and_load_dataset_with_extension('.nii.gz');
function test_fmri_io_nii()
save_and_load_dataset_with_extension('.nii');
function test_fmri_io_hdr()
save_and_load_dataset_with_extension('.hdr');
function test_fmri_io_img()
save_and_load_dataset_with_extension('.img');
function test_fmri_io_afni_orig_head()
if cosmo_skip_test_if_no_external('afni')
return
end
save_and_load_dataset_with_extension('+orig.HEAD');
function test_fmri_io_afni_tlrc_brik()
if cosmo_skip_test_if_no_external('afni')
return
end
save_and_load_dataset_with_extension('+tlrc.BRIK');
function test_fmri_io_bv_vmp()
if cosmo_skip_test_if_no_external('neuroelf')
return
end
save_and_load_dataset_with_extension('.vmp');
function test_fmri_io_bv_vmr()
if cosmo_skip_test_if_no_external('neuroelf')
return
end
save_and_load_dataset_with_extension('.vmr', false);
function test_fmri_io_bv_msk()
if cosmo_skip_test_if_no_external('neuroelf')
return
end
save_and_load_dataset_with_extension('.msk', false);
function test_fmri_io_mat()
save_and_load_dataset_with_extension('.mat');
function test_fmri_io_mat_struct()
ds = cosmo_synthetic_dataset('type', 'fmri');
fn = cosmo_make_temp_filename('fn1', '.mat');
cleaner = onCleanup(@()delete_files({fn}));
% save as struct
save(fn, '-struct', 'ds');
x = cosmo_fmri_dataset(fn);
assert_dataset_equal(x, ds, '.mat');
% save as variable
save(fn, 'ds');
x = cosmo_fmri_dataset(fn);
assert_dataset_equal(x, ds, '.mat');
function test_fmri_io_bv_vmp_oblique()
if cosmo_skip_test_if_no_external('neuroelf')
return
end
ds = cosmo_synthetic_dataset('size', 'normal', 'ntargets', 1, 'nchunks', 1);
% make dataset oblique (manually)
ds.a.vol.mat(1, 1) = .8 * 2;
ds.a.vol.mat(2, 1) = .6 * 2;
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_map2fmri(varargin{:}), '');
aet(ds, '-bv_vmp');
aet(ds, '-bv_vmp', 'deoblique', false);
ds_deob = cosmo_fmri_deoblique(ds);
bv_vmp = cosmo_map2fmri(ds, '-bv_vmp', 'deoblique', true);
cleaner = onCleanup(@()bv_vmp.ClearObject());
ds2 = cosmo_fmri_dataset(bv_vmp);
ds3 = cosmo_fmri_reorient(ds2, cosmo_fmri_orientation(ds_deob));
% require that rotation matrix is ok
assert_dataset_equal(ds3, ds_deob, '.vmr');
function test_fmri_io_bv_vmp_noniso()
if cosmo_skip_test_if_no_external('neuroelf')
return
end
ds = cosmo_synthetic_dataset('size', 'normal', 'ntargets', 1, 'nchunks', 1);
% make dataset oblique (manually)
ds.a.vol.mat(1, 1) = 1;
ds.a.vol.mat(2, 2) = 3;
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_map2fmri(varargin{:}), '');
aet(ds, '-bv_vmp');
aet(ds, '-bv_vmp', 'deoblique', false);
aet(ds, '-bv_vmp', 'deoblique', true);
aet(ds, '-bv_vmp', 'deoblique', true, 'bv_force_fit', false);
bv_vmp = cosmo_map2fmri(ds, '-bv_vmp', 'bv_force_fit', true);
cleaner = onCleanup(@()bv_vmp.ClearObject());
ds2 = cosmo_fmri_dataset(bv_vmp);
ds3 = cosmo_fmri_reorient(ds2, cosmo_fmri_orientation(ds));
vox_size = diag(ds.a.vol.mat);
resolution = prod(vox_size(1:3)).^(1 / 3);
assertElementsAlmostEqual(ds3.a.vol.mat(1:3, 1:3), ...
eye(3) * resolution);
ds.a.vol.mat = ds3.a.vol.mat;
% require that rotation matrix is ok
assert_dataset_equal(ds3, ds, '.vmr');
function test_map2fmri_illegal_name()
ds = cosmo_synthetic_dataset();
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_map2fmri(varargin{:}), '');
aet(ds, '-nii-nii');
aet(ds, '-illegalformat');
aet(ds, 'fn.illegal_extension');
function test_fmri_io_force_fit()
ds = cosmo_synthetic_dataset();
ds.a.vol.mat(2, 1) = 1 + rand();
assertFalse(has_isotropic_voxels(ds));
% just converting to nifti and back does not make the voxels isotropic
ds2 = cosmo_fmri_dataset(cosmo_map2fmri(ds, '-nii'));
assertFalse(has_isotropic_voxels(ds2));
% forcefit makes the voxels isotropic
ds3 = cosmo_fmri_dataset(cosmo_map2fmri(ds, '-nii', 'bv_force_fit', true));
assertTrue(has_isotropic_voxels(ds3));
function tf = has_isotropic_voxels(ds)
% helper function
m = ds.a.vol.mat;
sz = sum(m(1:3, 1:3).^2, 1);
eps = 1e-4;
tf = all(abs(sz(1) - sz) < eps);
function test_map2fmri_afni_nonisotropic
if cosmo_skip_test_if_no_external('afni')
return
end
ds = cosmo_synthetic_dataset();
% fine because voxels are not oblique
cosmo_map2fmri(ds, '-afni');
% make oblique
ds.a.vol.mat(2) = 1;
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_map2fmri(varargin{:}), '');
aet(ds, '-afni');
aet(ds, '-afni', 'deoblique', false);
% should be fine with de-obliqued
cosmo_map2fmri(ds, '-afni', 'deoblique', true);
function save_and_load_dataset_with_extension(ext, multi_volume)
if nargin < 2
multi_volume = true;
end
x_base = get_base_dataset();
if multi_volume
y = save_and_load(x_base, ext);
assert_dataset_equal(x_base, y, ext);
volumes = [4 2];
else
assertExceptionThrown(@()save_and_load(x_base, ext), '');
volumes = 1;
end
x_sel = cosmo_slice(x_base, volumes);
if multi_volume
x = x_base;
else
x = x_sel;
end
y_sel = save_and_load(x, ext, 'volumes', volumes);
assert_dataset_equal(x_sel, y_sel, ext);
if ext_supports_block_loading(ext)
helper_test_save_and_load_in_blocks(x, ext);
end
function tf = ext_supports_block_loading(ext)
ext_with_block_loading = {'+orig.HEAD', '+orig.BRIK', ...
'+tlrc.HEAD', '+tlrc.BRIK', ...
'.nii'};
tf = cosmo_match({ext}, ext_with_block_loading);
function helper_test_save_and_load_in_blocks(x, ext)
msk_ds = cosmo_slice(x, 1);
msk_ds.samples = x.fa.i > 3 & x.fa.j < 5;
ds_once = save_and_load(x, ext, 'mask', msk_ds);
[nsamples, nfeatures] = size(ds_once.samples);
block_sizes = [0, 1, 2 * nfeatures, (nsamples + 1) * nfeatures, Inf];
for block_size = block_sizes
ds_blocked = save_and_load(x, ext, 'mask', msk_ds, ...
'block_size', block_size);
assert_dataset_equal(ds_once, ds_blocked, ext);
end
function test_fmri_io_exceptions()
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_fmri_dataset(varargin{:}), '');
fn = cosmo_make_temp_filename('test', '.mat');
cleaner = onCleanup(@()delete_files({fn}));
x = struct();
save(fn, 'x');
aet(fn);
x = 1;
y = 1;
save(fn, 'x', 'y');
aet(fn);
% incomplete AFNI struct
x = struct();
x.DATASET_DIMENSIONS = [];
x.DATASET_RANK = [];
aet(x);
% illegal .mat file
save(fn, 'x');
aet(fn);
% wrong dimension size
ds = cosmo_synthetic_dataset();
nii = cosmo_map2fmri(ds, '-nii');
nii.img = zeros(1, 1, 1, 2, 5);
aet(nii);
function test_fmri_io_mask
aet = @(varargin)assertExceptionThrown(@() ...
cosmo_fmri_dataset(varargin{:}), '');
ds = cosmo_synthetic_dataset();
ds.sa = struct();
m = cosmo_slice(ds, 1);
fn = cosmo_make_temp_filename('fn1', '.nii');
fn2 = cosmo_make_temp_filename('fn2', '.nii');
cleaner = onCleanup(@()delete_files({fn, fn2}));
cosmo_map2fmri(ds, fn);
cosmo_map2fmri(m, fn2);
x = cosmo_fmri_dataset(fn, 'mask', fn2);
assert_dataset_equal(x, ds, '.nii');
m_rsa = cosmo_fmri_reorient(m, 'RSA');
cosmo_map2fmri(m_rsa, fn2);
x = cosmo_fmri_dataset(fn, 'mask', fn2);
assert_dataset_equal(x, ds, '.nii');
m_bad_fa = m;
m_bad_fa.a.fdim.values{1} = [1 2 3 4];
m_bad_fa.a.vol.dim(1) = 4;
m_bad_fa.fa.i = m.fa.i + 1;
cosmo_map2fmri(m_bad_fa, fn2);
aet(fn, 'mask', fn2);
m_bad_mat = m;
m_bad_mat.a.vol.mat(1) = 3;
cosmo_map2fmri(m_bad_mat, fn2);
aet(fn, 'mask', fn2);
m_bad_2samples = cosmo_stack({m, m});
cosmo_map2fmri(m_bad_2samples, fn2);
aet(fn, 'mask', fn2);
% disable automask warning
warning_state = cosmo_warning();
state_resetter = onCleanup(@()cosmo_warning(warning_state));
cosmo_warning('off');
ds.samples(:, 1:5) = 0;
res = cosmo_fmri_dataset(ds, 'mask', '');
assertEqual(res, ds);
ds.samples(1, 1) = 1;
res = cosmo_fmri_dataset(ds, 'mask', '');
assertEqual(res, ds);
msk = ds;
msk.samples = logical([0 1 1 0 1 0]);
msk = rmfield(msk, 'sa');
ds = cosmo_fmri_dataset(msk, 'mask', msk);
ds_expected = cosmo_slice(msk, msk.samples, 2);
assertEqual(ds, ds_expected);
function test_fmri_io_spm()
directories = {'', cosmo_make_temp_filename()};
cellfun(@helper_test_fmri_io_spm_in_directory, directories);
function helper_test_fmri_io_spm_in_directory(directory)
cleaner = onCleanup(@()register_or_delete_all_files());
[spm_fn, SPM] = write_spm_dot_mat(directory);
ds = cosmo_synthetic_dataset('ntargets', 2, 'nchunks', 1);
ds_spm = cosmo_fmri_dataset(spm_fn);
assertEqual(ds.sa.chunks, ds_spm.sa.chunks);
assertEqual(ds_spm.sa.beta_index, (1:2)');
assertEqual(ds_spm.sa.labels, {'sample_1'; 'sample_2'});
assertElementsAlmostEqual(ds.samples + 1, ds_spm.samples, 'relative', 1e-4);
msk_ds = cosmo_slice(ds, 1);
msk_ds.samples = msk_ds.fa.i ~= 3;
block_sizes = [0, 1, 6, 13, Inf];
ds_spm_msk = cosmo_fmri_dataset(spm_fn, 'mask', msk_ds);
assertEqual(ds_spm_msk, cosmo_slice(ds_spm, msk_ds.samples, 2));
for block_size = block_sizes
ds_spm_msk_blocked = cosmo_fmri_dataset(spm_fn, 'mask', msk_ds, ...
'block_size', block_size);
assertEqual(ds_spm_msk_blocked, ds_spm_msk);
end
input_keys = {'beta', 'con', 'spm'};
for k = 0:numel(input_keys)
if k == 0
postfix = '';
offset = 1;
else
key = input_keys{k};
postfix = [':' key];
offset = k;
end
spm_fn_key = [spm_fn postfix];
ds_spm = cosmo_fmri_dataset(spm_fn_key);
assertElementsAlmostEqual(ds.samples + offset, ds_spm.samples, ...
'relative', 1e-4);
is_beta = k <= 1;
if is_beta && isempty(directory)
ds_spm2 = cosmo_fmri_dataset(SPM);
assertEqual(ds_spm2, ds_spm);
end
end
function [spm_fn, SPM] = write_spm_dot_mat(directory)
register_or_delete_all_files();
prefix_with_directory = @(fn)fullfile(directory, fn);
if ~isempty(directory)
mkdir(directory);
end
ds = cosmo_synthetic_dataset('ntargets', 2, 'nchunks', 1);
nsamples = size(ds.samples, 1);
input_types.beta.vols = 'Vbeta';
input_types.con.vols = 'Vcon';
input_types.spmT.vols = 'Vspm';
input_keys = fieldnames(input_types);
SPM = struct();
SPM.SPMid = [];
xCon_cell = cell(1, nsamples);
for k = 1:numel(input_keys)
key = input_keys{k};
is_beta = strcmp(key, 'beta');
values = input_types.(key);
vol_label = values.vols;
samples_offset = k;
vol_cell = cell(1, nsamples);
sample_labels = cell(1, nsamples);
for j = 1:nsamples
tmp_fns = cosmo_make_temp_filename('XX', {'.hdr', '.img', '.mat'});
tmp_hdr_fn = tmp_fns{1};
tmp_pth_fns = cellfun(prefix_with_directory, tmp_fns, ...
'UniformOutput', false);
tmp_hdr_pth_fn = tmp_pth_fns{1};
tmp_img_pth_fn = tmp_pth_fns{2};
tmp_mat_pth_fn = tmp_pth_fns{3};
vol = ds.a.vol;
vol.fname = tmp_hdr_fn;
vol_cell{j} = vol;
ds_sample = cosmo_slice(ds, j);
ds_sample.samples = ds_sample.samples + samples_offset;
cosmo_map2fmri(ds_sample, tmp_hdr_pth_fn);
sample_labels{j} = sprintf('sample_%d', j);
register_or_delete_all_files(tmp_hdr_pth_fn);
register_or_delete_all_files(tmp_img_pth_fn);
register_or_delete_all_files(tmp_mat_pth_fn);
if ~is_beta
xcon = xCon_cell{j};
if isempty(xcon)
xcon = struct();
xcon.name = sample_labels{j};
end
xcon.(vol_label) = vol;
xCon_cell{j} = xcon;
end
end
vol_info = cat(2, vol_cell{:});
if is_beta
SPM.(vol_label) = vol_info;
end
if is_beta
SPM.xX.X = [];
SPM.Sess.Fc.i = 1:2;
SPM.Sess.col = 1:2;
SPM.xX.name = sample_labels;
end
end
SPM.xCon = cat(1, xCon_cell{:});
spm_fn = prefix_with_directory('_tmp_SPM.mat');
register_or_delete_all_files(spm_fn);
save(spm_fn, 'SPM');
function register_or_delete_all_files(fn)
persistent files_to_delete
has_files_to_delete = iscell(files_to_delete);
if nargin == 0
if has_files_to_delete
delete_files(files_to_delete);
end
files_to_delete = [];
else
if ~has_files_to_delete
files_to_delete = cell(0, 1);
end
files_to_delete{end + 1} = fn;
end
function assert_dataset_equal(x, y, ext)
funcs = {@assert_samples_equal, @assert_a_equal, ...
@assert_fa_equal, ...
@assert_sa_equal};
for j = 1:numel(funcs)
func = funcs{j};
func(x, y, ext);
end
function assert_samples_equal(x, y, ext)
xs = x.samples;
ys = y.samples;
switch ext
case '.msk'
assert(cosmo_corr(xs', ys') > .999);
case '.vmr'
assert(cosmo_corr(xs', ys') > .999);
otherwise
assertElementsAlmostEqual(xs, ys, 'relative', 1e-5);
end
function assert_a_equal(xd, yd, ext)
x = xd.a;
y = yd.a;
x.vol = rmfield_if_present(x.vol, 'xform');
y.vol = rmfield_if_present(y.vol, 'xform');
switch ext
case '.vmr'
% does not store the offset, so do not check it
x.vol.mat = x.vol.mat(:, 1:3);
y.vol.mat = y.vol.mat(:, 1:3);
otherwise
% keep as is
end
assertEqual(x, y);
function assert_fa_equal(x, y, ext)
assertEqual(x.fa, y.fa);
function assert_sa_equal(x, y, ext)
ignore_sa_exts = {'.nii', '.nii.gz', '.hdr', '.img', '.vmr', '.msk'};
if cosmo_match({ext}, ignore_sa_exts)
return
end
assertEqual(x.sa, y.sa);
function s = rmfield_if_present(s, f)
if isfield(s, f)
s = rmfield(s, f);
end
function x = get_base_dataset()
x = cosmo_synthetic_dataset('size', 'big');
x = cosmo_fmri_reorient(x, 'ASR');
x = cosmo_slice(x, 1:4);
x.sa = struct();
x.sa.labels = {'labels1'; 'labels2'; 'labels3'; 'labels4'};
x.sa.stats = {'Beta(17,2)'; ''; 'Zscore()'; 'Ftest(2,3)'};
function ds_again = save_and_load(ds, ext, varargin)
directories = {'', cosmo_make_temp_filename()};
ds_again_cell = cellfun(@(directory)save_and_load_in_directory( ...
directory, ds, ext, varargin), directories, ...
'UniformOutput', false);
ds_again = ds_again_cell{1};
% all datasets must give the same dataset
assertTrue(all(cellfun(@(ds)isequal(ds_again, ds), ...
ds_again_cell(2:end))));
function ds_again = save_and_load_in_directory(directory, ds, ext, varargin)
sib_exts = get_sibling_exts(ext);
all_exts = [{ext} sib_exts];
temp_fns = cosmo_make_temp_filename('', all_exts);
temp_pth_fns = cellfun(@(fn)fullfile(directory, fn), temp_fns, ...
'UniformOutput', false);
if ~isempty(directory)
mkdir(directory);
end
main_temp_pth_fn = temp_pth_fns{1};
cleaner = onCleanup(@()delete_files(temp_pth_fns));
switch ext
case '.mat'
save(main_temp_pth_fn, 'ds');
otherwise
% disable automask warning
warning_state = cosmo_warning();
state_resetter = onCleanup(@()cosmo_warning(warning_state));
cosmo_warning('off');
cosmo_map2fmri(ds, main_temp_pth_fn);
end
ds_again = cosmo_fmri_dataset(main_temp_pth_fn, varargin{:});
function sib_exts = get_sibling_exts(ext)
sib_exts = cell(1, 0);
switch ext
case '.img'
sib_exts = {'.hdr', '.mat'};
case '.hdr'
sib_exts = {'.img', '.mat'};
case '+orig.HEAD'
sib_exts = {'+orig.BRIK'};
case '+orig.BRIK'
sib_exts = {'+orig.HEAD'};
case '+tlrc.HEAD'
sib_exts = {'+tlrc.BRIK'};
case '+tlrc.BRIK'
sib_exts = {'+tlrc.HEAD'};
case '.vmp'
% recent Neuroelf (>=v1.1) seems to save run time variables
% in an .rtv file
sib_exts = {'.rtv'};
case {'.msk', '.mat', '.vmr', '.nii', '.nii.gz'}
% do nothing
otherwise
error('unsupported extension ''%s''', ext);
end
function delete_files(fns)
% delete files. If they are all in the same directory and the
% directory is not empty, then the directory is removed as well
for k = 1:numel(fns)
fn = fns{k};
if ~isempty(fn) && exist(fn, 'file')
delete(fn);
end
pth = fileparts(fn);
if k == 1
first_pth = pth;
elseif ~isequal(pth, first_pth)
error('paths differ: %s ~= %s', first_pth, pth);
end
end
if ~isempty(pth)
rmdir(pth);
end