function db = cosmo_meeg_layout_collection()
% return supported MEEG channel layouts
%
% db=cosmo_meeg_layout_collection()
%
% Output:
% db Nx1 cell for N layouts. Each elements has the following
% fields:
% .pos Mx2 x and y coordinates for M channels
% .width Mx1 width of channel
% .height Mx1 height of channel
% .label Mx1 cell string with channel labels
% .name string with filename of layout
%
% Example:
% % (This example requires FieldTrip)
% cosmo_skip_test_if_no_external('fieldtrip');
% %
% % get all layouts
% layouts=cosmo_meeg_layout_collection();
% %
% % find index of neuromag306cmb layout
% names=cellfun(@(x)x.name,layouts,'UniformOutput',false);
% i=find(cosmo_match(names,'neuromag306cmb.lay'));
% %
% % show neuromag306 combined planar layout (with 102 channels)
% pl_layout=layouts{i};
% cosmo_disp(pl_layout.label)
% %|| { 'MEG0112+0113'
% %|| 'MEG0122+0123'
% %|| 'MEG0132+0133'
% %|| :
% %|| 'MEG2642+2643'
% %|| 'COMNT'
% %|| 'SCALE' }@104x1
% cosmo_disp([pl_layout.pos pl_layout.width pl_layout.height])
% %|| [ -0.408 0.273 0.0717 0.0791
% %|| -0.328 0.306 0.0717 0.0791
% %|| -0.377 0.179 0.0717 0.0791
% %|| : : : :
% %|| 0.373 -0.104 0.0717 0.0791
% %|| -0.547 -0.5 0.0717 0.0791
% %|| 0.547 -0.5 0.0717 0.0791 ]@104x4
% pl_layout.name
% %|| 'neuromag306cmb.lay'
%
% Note:
% - this function requires FieldTrip, as it uses its collection of
% layouts
% - the output from this function is similar to FieldTrip's
% ft_prepare_layout.
% - this function caches previously read layouts, for optimization
% reasons. run "clear functions" to reset the cache.
%
% # For CoSMoMVPA's copyright information and license terms, #
% # see the COPYING file distributed with CoSMoMVPA. #
% cache the layouts, so that after the first call layouts don't have to
% be read from disc
persistent cached_layout_db
if isempty(cached_layout_db)
cached_layout_db = read_all_layouts();
end
db = cached_layout_db;
function layouts = read_all_layouts()
cosmo_check_external('fieldtrip');
% see where fieldtrip is
ft_dir = fileparts(which('ft_defaults'));
lay_dir = fullfile(ft_dir, 'template', 'layout');
% get layout filenames
lay_fns = dir(fullfile(lay_dir, '*.lay'));
nlay = numel(lay_fns);
if nlay == 0
error('No layout founds in %s', lay_dir);
end
layout_names = {lay_fns.name}';
% allocate space for output
layouts = cell(nlay, 1);
% read each layout
for k = 1:nlay
lay_name = layout_names{k};
lay_fn = fullfile(lay_dir, lay_name);
lay = cosmo_meeg_read_layout(lay_fn);
lay.name = lay_name;
layouts{k} = lay;
end
layouts = apply_fixers(layouts);
function layouts = apply_fixers(layouts)
fixers = {@fix_elec1020_old_fieldtrip, ...
@fix_make_ft_compatible};
for j = 1:numel(fixers)
fixer = fixers{j};
layouts = fixer(layouts);
end
function db = fix_elec1020_old_fieldtrip(db)
% old fieldtrip had channel locations differently;
% fix that there so that the unit tests pass
i = find_layout(db, 'elec1020.lay');
lay = db{i};
if isequal(lay.pos(1, :), [-0.308949 0.951110]) && ...
isequal(unique(lay.width), .75)
suffix = cellfun(@(x)x(end), lay.label)';
suffix1 = suffix == '1';
suffix2 = suffix == '2';
assert(isequal(lay.label(suffix1), {'Fp1', 'O1'}'));
assert(isequal(lay.label(suffix2), {'Fp2', 'O2'}'));
lay.pos(suffix1, :) = [-.38 .89111; -.38 -.89111];
lay.pos(suffix2, :) = [.38 .891004; .38 -.891004];
lay.width(:) = .35;
lay.height(:) = .25;
db{i} = lay;
end
function i = find_layout(db, name)
names = cellfun(@(x)x.name, db, 'UniformOutput', false);
i = find(cosmo_match(names, name));
assert(numel(i) == 1);
function layouts = fix_make_ft_compatible(layouts)
for j = 1:numel(layouts)
layouts{j} = make_single_ft_compatible(layouts{j});
end
function layout = make_single_ft_compatible(layout)
processors = {@layout_ft_add_outline, ...
@layout_ft_scale, ...
@layout_ft_add_misc_labels};
for k = 1:numel(processors)
processor = processors{k};
layout = processor(layout);
end
function layout = layout_ft_add_outline(layout)
% add outline and mask to the layout
alpha = (0:.01:1)' * (2 * pi);
head = [cos(alpha) sin(alpha)] * .5;
nose = [0.09 0.496
0 0.575
-0.09 0.496];
right_ear = [0.4970 0.0555
0.5100 0.0775
0.5180 0.0783
0.5299 0.0746
0.5419 0.0555
0.5400 -0.0055
0.5470 -0.0932
0.5320 -0.1313
0.5100 -0.1384
0.4890 -0.1199];
left_ear = bsxfun(@times, [-1 1], right_ear);
layout.outline = {head, nose, right_ear, left_ear};
layout.mask = {head};
function layout = layout_ft_scale(layout)
% do not consider the COMNT and SCALE channels
chan_msk = ~cosmo_match(layout.label, {'COMNT', 'SCALE'});
chan_pos = layout.pos(chan_msk, :);
extent = (max(chan_pos) - min(chan_pos));
% put everything in circle around origin with radius .45
layout.pos = (bsxfun(@rdivide, bsxfun(@minus, ...
layout.pos, min(chan_pos)), extent)) * .9 - .45;
layout.width = layout.width / extent(1);
layout.height = layout.height / extent(2);
function stacked_layout = stack_channels(layouts)
% helper function to stack the channels
% if any element is empty it is just ignored
keys = {'pos', 'width', 'height', 'label'};
nlayout = numel(layouts);
nkeys = numel(keys);
stacked_layout = layouts{1};
for k = 1:nkeys
key = keys{k};
values_cell = cell(nlayout, 1);
keep = false(nlayout, 1);
for j = 1:nlayout
layout = layouts{j};
if ~isempty(layout)
values_cell{j} = layout.(key);
keep(j) = true;
end
end
stacked_layout.(key) = cat(1, values_cell{keep});
end
function layout = layout_ft_add_misc_labels(layout)
comnt_layout = get_single_label(layout, 'COMNT', [-.45 -.45]);
scale_layout = get_single_label(layout, 'SCALE', [.45 -.45]);
layout = stack_channels({layout, comnt_layout, scale_layout});
function y = get_single_label(x, label, pos)
% return a layout
if cosmo_match({label}, x.label)
y = [];
else
y.width = mean(x.width, 1);
y.height = mean(x.height, 1);
y.label = {label};
y.pos = pos;
end