Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 97 additions & 23 deletions CanlabCore/@fmri_surface_data/surface.m
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,15 @@
% image_vector.surface. Three modes:
%
% 1. NATIVE (default): builds a managed, stateful fmridisplay whose surface
% views are the mesh set that matches the object's surface_space (fs_LR-32k
% -> 'foursurfaces_hcp', fsaverage-164k -> 'foursurfaces_freesurfer'), then
% paints the data as a MANAGED surface-native layer (colored DIRECTLY from
% the per-vertex data -- no resampling; medial wall and zeros render gray).
% Because the returned object is a stateful fmridisplay under a controller,
% set_colormap / set_opacity / rethreshold / removeblobs / refresh act on
% the surfaces (this is why the colormap is now changeable after the fact).
% views are the four-surface set that MATCHES the object's surface_space
% (see "Surface spaces" below), then paints the data as a MANAGED
% surface-native layer (colored DIRECTLY from the per-vertex data -- no
% resampling when the mesh is the object's own space; medial wall and zeros
% render gray). Because the returned object is a stateful fmridisplay under
% a controller, set_colormap / set_opacity / rethreshold / removeblobs /
% refresh act on the surfaces (this is why the colormap is changeable after
% the fact). With NO explicit surface argument, the default view is chosen
% to match the object's space (below); passing a surface keyword overrides it.
%
% 2. EXISTING SURFACE ('existingsurface', han): colors patch handles you
% already have (e.g. from addbrain or a prior surface call). Matching-space
Expand All @@ -30,11 +32,40 @@
% 'left', 'right', 'hcp inflated') and renders onto it, projecting to a
% volume when the surface is not the object's native mesh.
%
% :Surface spaces and their default display meshes:
%
% The object's .surface_space determines the default four-surface view. Two
% spaces have NATIVE display meshes bundled with CanlabCore and paint directly;
% the rest render on the four-surface view of their parent / aligned space via
% a fast, cached nearest-neighbour resample (a one-line message prints when
% that happens). All mesh files live under
% canlab_canonical_brains/Canonical_brains_surfaces/.
%
% ======================================================================================
% surface_space verts/hemi default view display mesh files
% ======================================================================================
% fsLR_32k 32492 foursurfaces_hcp S12000.L/R.inflated_MSMAll.32k_fsl_LR.mat (native)
% fsaverage_164k 163842 foursurfaces_freesurfer surf_freesurf_inflated_Left/Right.mat (native)
% fsaverage6 40962 foursurfaces_freesurfer (resampled up to fsaverage-164k, then above)
% fsaverage5 10242 foursurfaces_freesurfer (resampled up to fsaverage-164k, then above)
% fsaverage4 2562 foursurfaces_freesurfer (resampled up to fsaverage-164k, then above)
% onavg_41k 40962 foursurfaces_hcp (onavg is fs_LR-aligned -> resampled to fs_LR)
% onavg_10k 10242 foursurfaces_hcp (onavg is fs_LR-aligned -> resampled to fs_LR)
% ======================================================================================
%
% Other fs_LR surftypes (fsLR_32k only): 'midthickness' uses
% S1200.L/R.midthickness_MSMAll.32k_fs_LR.surf.gii and 'sphere' uses
% S1200.L/R.sphere.32k_fs_LR.mat (see 'surftype' below). fsaverage-164k has an
% inflated mesh only. To render in a space with no bundled mesh at true native
% fidelity, resample first with resample_surface (see :See also).
%
% :Optional Inputs:
% **'surftype':** 'inflated' (default), 'midthickness', 'sphere' (fs_LR).
% **'surftype':** 'inflated' (default), 'midthickness', 'sphere' (fs_LR only).
% **'which_image':** map (column) to render. Default 1.
% **'existingsurface':** vector of patch handles to color.
% **'mni_surface':** an addbrain keyword (string).
% Any addbrain surface keyword given as a bare token (e.g. 'foursurfaces_hcp',
% 'hcp inflated left') overrides the space-matched default view.
%
% Color options (harmonized with the volume pipeline; forwarded to
% render_on_surface -- see there for details):
Expand All @@ -54,24 +85,60 @@
%
% :Examples:
% ::
% % ---- Native four-surface render, one example per surface space ----
% % Each object's space picks its matching four-surface view automatically.
%
% % fs_LR-32k (HCP CIFTI grayordinates) -> foursurfaces_hcp
% s = fmri_surface_data(which('S1200.MyelinMap_MSMAll.32k_fs_LR.dscalar.nii'));
% surface(s, 'which_image', 1);
% surface(s);
%
% v = ttest(load_image_set('emotionreg')); % a volumetric statistic_image
% surface(vol2surf(v)); % native fsaverage render
% surface(vol2surf(v), 'mni_surface', 'left'); % on an addbrain MNI surface
% % fsaverage-164k (from a volume via CBIG registration fusion) -> foursurfaces_freesurfer
% v = ttest(load_image_set('emotionreg')); % a volumetric statistic_image
% sf = vol2surf(v); % fsaverage-164k object
% surface(sf, 'clim', [-4 4]);
%
% :See also: render_on_surface, load_surface_geom, addbrain, fmri_surface_data
% % Nested fsaverage6 (resamples up onto the fsaverage-164k four-surface view)
% s6 = resample_surface(s, 'fsaverage6');
% surface(s6);
%
% % onavg (equal-area; fs_LR-aligned, so it renders on the fs_LR view)
% son = resample_surface(s, 'onavg_41k');
% surface(son);
%
% % Other fs_LR surftypes (fsLR_32k meshes only)
% surface(s, 'surftype', 'midthickness');
% surface(s, 'surftype', 'sphere');
%
% % ---- Rendering onto a specific existing / MNI surface ----
% surface(s, 'foursurfaces_hcp'); % explicit view (overrides default)
% surface(sf, 'mni_surface', 'left'); % an addbrain MNI pial surface (via volume)
% hp = addbrain('hcp inflated left'); % a native fs_LR patch
% surface(s, 'existingsurface', hp); % color it directly (no resampling)
%
% :See also: render_on_surface, resample_surface, load_surface_geom, addbrain, fmri_surface_data

surftype = 'inflated';
which_image = 1;
existing = [];
mni_surface = '';
coloropts = {}; % forwarded to render_on_surface (harmonized colors)
coloropts = {}; % colour opts -> render_on_surface / the managed layer
surf_args = {}; % surface directives (which view) -> @fmridisplay/surface

% Colour options that take a VALUE (consume a pair). Everything else that is not
% a recognized mode keyword is a surface directive forwarded to the native
% managed path -- either a bare addbrain/foursurfaces keyword (single token) or a
% 'direction'/'orientation'/'axes' pair -- so surface(obj, 'foursurfaces_hcp'),
% surface(obj, 'hcp inflated left'), etc. override the space-matched default view.
color_value_keys = {'clim', 'cmaprange', 'colormap', 'colormapname', ...
'pos_colormap', 'neg_colormap', 'splitcolor', 'maxcolor', 'mincolor', ...
'color', 'transvalue'};
surf_pair_keys = {'direction', 'orientation', 'axes'};

i = 1;
while i <= numel(varargin)
switch lower(char(varargin{i}))
tok = varargin{i};
key = ''; if ischar(tok) || isstring(tok), key = lower(char(tok)); end
switch key
case 'surftype', surftype = varargin{i+1}; i = i + 2;
case 'which_image', which_image = varargin{i+1}; i = i + 2;
case {'existingsurface','surface_handles'}, existing = varargin{i+1}; i = i + 2;
Expand All @@ -80,12 +147,16 @@
coloropts = [coloropts, varargin(i)]; %#ok<AGROW>
i = i + 1;
otherwise
% Any other option (clim, colormap, cmaprange, pos_colormap /
% neg_colormap, splitcolor, maxcolor / mincolor, color, ...) is
% forwarded to render_on_surface, which harmonizes them with the
% volume visualization color pipeline.
coloropts = [coloropts, varargin(i:i+1)]; %#ok<AGROW>
i = i + 2;
if any(strcmp(key, color_value_keys))
coloropts = [coloropts, varargin(i:i+1)]; %#ok<AGROW>
i = i + 2;
elseif any(strcmp(key, surf_pair_keys))
surf_args = [surf_args, varargin(i:i+1)]; %#ok<AGROW>
i = i + 2;
else
surf_args = [surf_args, varargin(i)]; %#ok<AGROW> % bare surface directive
i = i + 1;
end
end
end

Expand Down Expand Up @@ -113,9 +184,12 @@
% on the returned object). The heavy lifting -- pick the matching surfaces, add
% them, paint at full fidelity -- is done by @fmridisplay/surface's
% fmri_surface_data path, so there is a single code path for surface(obj) and
% surface(o2, obj).
% surface(o2, obj). A surf_args entry (e.g. 'foursurfaces_hcp', a bare addbrain
% keyword, or a direction/orientation/axes pair) overrides the space-matched
% default view; with none, @fmridisplay/surface picks the matching four-surface
% set (see surface_default_keyword).
o2 = fmridisplay;
o2 = surface(o2, obj, ropts{:});
o2 = surface(o2, obj, ropts{:}, surf_args{:});

% Honor a non-default surftype (midthickness / sphere) by swapping the managed
% patches' geometry to the requested mesh. The foursurfaces keyword draws
Expand Down
7 changes: 5 additions & 2 deletions CanlabCore/@fmri_surface_data/to_display_volume.m
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,11 @@
fs = obj;
if ~strcmp(obj.surface_space, 'fsaverage_164k')
% surf2vol needs fsaverage (the CBIG warp is fsaverage-based); get there
% natively. resample_surface auto-selects nearest for label/binary data.
fs = resample_surface(obj, 'fsaverage_164k');
% natively. This volume is for DISPLAY ONLY (sampled at mesh vertices to
% colour a patch), so resample by nearest neighbour: at 164k density the
% visual result is indistinguishable from barycentric, but ~250x faster
% (0.2 s vs ~48 s -- the barycentric weight build dominates otherwise).
fs = resample_surface(obj, 'fsaverage_164k', 'interp', 'nearest');
end
volc = surf2vol(fs);
end
Expand Down
93 changes: 63 additions & 30 deletions CanlabCore/@fmridisplay/render_layer_surfaces.m
Original file line number Diff line number Diff line change
Expand Up @@ -242,58 +242,48 @@
hemiL(native_nv) = local_apply_thr(L0, thr);
hemiR(native_nv) = local_apply_thr(R0, thr);

% NOTE ON SPEED: a patch whose vertex count is the object's OWN space is painted
% directly (the common, fast path -- no resample/projection). Only a DIFFERENT
% standard cortical mesh triggers resample_surface, and only an arbitrary mesh in a
% view with NO native cortical match triggers the (slower) volume projection -- so
% e.g. the small subcortical anatomy meshes bundled into a foursurfaces_hcp layout
% do NOT force a projection when the cortex is already painted natively.
n_painted = 0; n_unknown = 0;
resample_msg_shown = false; project_msg_shown = false;
for i = wh_surface
if i < 1 || i > numel(obj.surface), continue; end
surfh = obj.surface{i}.object_handle;
surfh = surfh(ishandle(surfh));
view_cortex_painted = false; % a native/resampled cortical patch got the data
nonstd = gobjects(0); % arbitrary meshes deferred to volume projection
for hh = surfh(:)'
if ~strcmp(get(hh, 'Type'), 'patch'), continue; end
V = get(hh, 'Vertices');
nv = size(V, 1);
tag = lower(get(hh, 'Tag'));
if iscell(tag), tag = strjoin(tag, ' '); end

% Ensure hemisphere data exists for this mesh's vertex count (resample if
% it is a recognized standard space different from the object's own).
% Ensure hemisphere data exists for this mesh's vertex count.
if ~isKey(hemiL, nv)
tgt = local_std_space_name(nv);
if isempty(tgt)
% Non-standard MNI isosurface (e.g. addbrain 'hires left',
% 'cutaway'): no spherical registration exists, so paint it by
% projecting the data to a volume and sampling that volume at the
% patch's vertices (image_vector.render_on_surface). The volume is
% cached on the layer so re-renders stay fast.
if ~isfield(layer, 'display_volume') || isempty(layer.display_volume)
try
layer.display_volume = to_display_volume(surf);
obj.activation_maps{k}.display_volume = layer.display_volume;
catch
n_unknown = n_unknown + 1; continue;
end
end
vimg = layer.display_volume;
if size(vimg.dat, 2) > 1, vimg = get_wh_image(vimg, which_image); end
if ~isempty(thr) && isscalar(thr) && isfinite(thr) && ~strcmp(tc_map.type, 'indexed')
vimg.dat(abs(vimg.dat) < thr) = 0; % apply the layer threshold
end
cargs = {'truecolor', tc_map, 'truecolor_alpha', alpha, 'nolegend'};
if ~isempty(cmaprange), cargs = [cargs, {'cmaprange', cmaprange}]; end %#ok<AGROW>
if strcmp(tc_map.type, 'indexed'), cargs = [cargs, {'interp', 'nearest'}]; end %#ok<AGROW>
try
render_on_surface(vimg, hh, cargs{:});
n_painted = n_painted + 1;
catch
n_unknown = n_unknown + 1;
end
nonstd(end + 1) = hh; %#ok<AGROW> % arbitrary mesh: defer (project only if needed)
continue
end
% A DIFFERENT standard cortical mesh (e.g. fs_LR <-> fsaverage): resample
% the source onto it once, cached on the layer.
key = sprintf('nv%d', nv);
if ~isfield(layer.resampled, key)
if ~resample_msg_shown
fprintf('render_layer_surfaces: resampling %s data onto a %s mesh (once; cached)...\n', ...
surf.surface_space, tgt);
resample_msg_shown = true;
end
try
layer.resampled.(key) = resample_surface(surf, tgt, 'interp', 'nearest');
catch
n_unknown = n_unknown + 1; continue;
nonstd(end + 1) = hh; %#ok<AGROW> % fall back to volume projection
continue
end
obj.activation_maps{k}.resampled = layer.resampled; % persist for re-renders
end
Expand Down Expand Up @@ -337,6 +327,49 @@
out(col, :) = alpha * rgb(col, :) + (1 - alpha) * base(col, :);
set(hh, 'FaceVertexCData', out, 'FaceColor', 'interp', 'EdgeColor', 'none');
n_painted = n_painted + 1;
view_cortex_painted = true;
end

% Arbitrary (non-standard) meshes in this view are painted via a volume
% projection -- but ONLY if no native/resampled cortical patch already
% received the data in this view. This avoids the expensive resample +
% surf2vol round-trip for cortex-only data (e.g. fs_LR myelin) rendered on
% a mixed patch set like 'foursurfaces_hcp', where incidental subcortical
% anatomy meshes would otherwise trigger a needless projection.
if ~isempty(nonstd) && ~view_cortex_painted
if ~isfield(layer, 'display_volume') || isempty(layer.display_volume)
if ~project_msg_shown
fprintf(['render_layer_surfaces: projecting %s data to a volume to render ' ...
'on an arbitrary surface (once; cached)...\n'], surf.surface_space);
project_msg_shown = true;
end
try
layer.display_volume = to_display_volume(surf);
obj.activation_maps{k}.display_volume = layer.display_volume;
catch
layer.display_volume = [];
end
end
if ~isempty(layer.display_volume)
vimg = layer.display_volume;
if size(vimg.dat, 2) > 1, vimg = get_wh_image(vimg, which_image); end
if ~isempty(thr) && isscalar(thr) && isfinite(thr) && ~strcmp(tc_map.type, 'indexed')
vimg.dat(abs(vimg.dat) < thr) = 0;
end
cargs = {'truecolor', tc_map, 'truecolor_alpha', alpha, 'nolegend'};
if ~isempty(cmaprange), cargs = [cargs, {'cmaprange', cmaprange}]; end
if strcmp(tc_map.type, 'indexed'), cargs = [cargs, {'interp', 'nearest'}]; end
for hh = nonstd(:)'
try
render_on_surface(vimg, hh, cargs{:});
n_painted = n_painted + 1;
catch
n_unknown = n_unknown + 1;
end
end
else
n_unknown = n_unknown + numel(nonstd);
end
end
end

Expand Down
41 changes: 34 additions & 7 deletions CanlabCore/@fmridisplay/surface.m
Original file line number Diff line number Diff line change
Expand Up @@ -475,13 +475,40 @@


function kw = surface_default_keyword(surf_data)
% Multi-surface keyword whose meshes MATCH the object's surface space, so the
% data paints directly (no cross-space resampling). Used when surface(o2,
% surf_obj) is called with no explicit surface directive.
switch surf_data.surface_space
case 'fsLR_32k', kw = 'foursurfaces_hcp'; % 32492 verts/hemi
case 'fsaverage_164k', kw = 'foursurfaces_freesurfer'; % 163842 verts/hemi
otherwise, kw = 'foursurfaces_hcp'; % best-effort default
% Default four-surface view (L/R lateral + medial) whose meshes MATCH -- or best
% match -- the object's surface space, so the data paints at full fidelity with
% no (or a cheap, cached) resample. Used when surface(o2, surf_obj) is called
% with no explicit surface directive.
%
% Two spaces have native display meshes bundled with CanlabCore:
% fsLR_32k (32492 verts/hemi) -> foursurfaces_hcp (native paint)
% fsaverage_164k (163842 verts/hemi) -> foursurfaces_freesurfer (native paint)
%
% The other supported spaces have no bundled display mesh, so they render on the
% four-surface view of their PARENT / aligned display space (a fast, cached
% nearest-neighbour resample handled by render_layer_surfaces):
% fsaverage6/5/4 (nested icosahedra) -> foursurfaces_freesurfer (resample up to 164k)
% onavg_41k/10k (equal-area, fs_LR-aligned) -> foursurfaces_hcp (resample to fs_LR-32k)
switch local_space_family(surf_data.surface_space)
case 'fslr', kw = 'foursurfaces_hcp'; % fs_LR-32k (native)
case 'fsaverage', kw = 'foursurfaces_freesurfer'; % fsaverage-164k (native for 164k; 6/5/4 resample up)
case 'onavg', kw = 'foursurfaces_hcp'; % onavg is fs_LR-aligned -> render on fs_LR
otherwise, kw = 'foursurfaces_hcp'; % best-effort default
end
end


function fam = local_space_family(space)
% Coarse family of a surface_space string ('fslr' | 'fsaverage' | 'onavg' | '').
s = lower(char(space));
if contains(s, 'onavg')
fam = 'onavg';
elseif contains(s, 'fsaverage') || contains(s, 'fsavg')
fam = 'fsaverage';
elseif contains(s, 'fslr') || contains(s, 'fs_lr')
fam = 'fslr';
else
fam = '';
end
end

Loading
Loading