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
21 changes: 21 additions & 0 deletions CanlabCore/@fmridisplay/addblobs.m
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,15 @@
' o2 = addblobs(o2, region(t)); %% add the blobs from t']);
end

% Multi-image objects: a single blob layer shows one image, so if an
% image_vector with more than one image (column) is passed, use only the FIRST
% image (with a note). This keeps addblobs(o2, multi_image_obj) from erroring or
% silently combining images; use image_vector.montage for a per-image display.
if isa(cl, 'image_vector') && size(cl.dat, 2) > 1
fprintf('addblobs: object has %d images; showing the first only. (For a per-image display use montage(obj).)\n', size(cl.dat, 2));
cl = get_wh_image(cl, 1);
end

% Retain the ORIGINAL input as the layer source, before any conversion.
% A statistic_image/fmri_data source enables re-thresholding downward
% (rethreshold); a region source supports re-render at the same or higher
Expand Down Expand Up @@ -506,6 +515,18 @@
end


% Ensure a colour range exists even when there is NO montage (e.g. a
% surface-only object), so render_layer_surfaces and the controller's numeric
% legend both have one. With a montage the loop above already stored the
% render_blobs-computed range; here we fill the surface-only gap using the same
% shared, uniform default (split-aware) on the layer's mapped values.
if isempty(obj.activation_maps{wh_to_display}.cmaprange)
split_flag = {};
if any(strcmp(varargin, 'splitcolor')), split_flag = {'splitcolor'}; end
obj.activation_maps{wh_to_display}.cmaprange = ...
canlab_default_cmaprange(currentmap.mapdata(:), split_flag{:});
end

% Surfaces
% -------------------------------------------------------------------------
% Render this layer onto every (targeted) registered surface view. The actual
Expand Down
324 changes: 240 additions & 84 deletions CanlabCore/@fmridisplay/controller.m

Large diffs are not rendered by default.

40 changes: 34 additions & 6 deletions CanlabCore/@fmridisplay/fmridisplay.m
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,16 @@
obj.history_descrip = [];
obj.additional_info = '';

if nargin == 0
return
end

% Control-only flag: 'nocontroller' suppresses the auto-launched
% controller (below). Stripped here so it is not forwarded to the
% input parser. The controller auto-opens by default but never in
% -batch (so scripts / unit tests are unaffected).
do_controller = ~batchStartupOptionUsed;
wh_noctrl = strcmp(varargin, 'nocontroller');
if any(wh_noctrl), do_controller = false; varargin(wh_noctrl) = []; end

% Now parse inputs and run methods depending on what is entered

if any(strcmp(varargin, 'montage'))
wh = strcmp(varargin, 'montage');
varargin(wh) = [];
Expand Down Expand Up @@ -250,7 +253,32 @@
end
end


% Bring up the interactive controller by default (fmridisplay is a
% handle class, so it stays bound to this object and updates as layers
% are added). Suppressed in -batch and with 'nocontroller'; wrapped so
% it can never break construction. Save/restore the current figure so
% opening the controller uifigure does NOT hijack gcf — otherwise the
% very next montage/surface call (e.g. `figure; montage(dat)`) would
% render its slices into the controller window.
if do_controller
prevfig = get(groot, 'CurrentFigure');
try
controller(obj);
catch ME
warning('fmridisplay:controllerAutolaunch', ...
'Could not open display controller: %s', ME.message);
end
% Do not leave the controller uifigure as the current figure:
% downstream rendering (montage's bare axes(), addbrain's subplot)
% targets gcf and would draw into / error on the controller.
if ~isempty(prevfig) && isgraphics(prevfig) && isvalid(prevfig)
set(groot, 'CurrentFigure', prevfig); % back to the caller's figure
elseif isgraphics(obj.controller_handle) && ...
isequal(get(groot, 'CurrentFigure'), obj.controller_handle)
figure; % no prior figure: hand off a fresh one
end
end

end % constructor function

end % methods
Expand Down
142 changes: 117 additions & 25 deletions CanlabCore/@fmridisplay/montage.m
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,56 @@

if isempty(obj.SPACE) || ~isstruct(obj.SPACE.V)
error('fmridisplay is not initialized correctly. run obj = fmridisplay; first and then pass in to this method.')


end

% Multi-panel routing
% -------------------------------------------------------------------------
% The default montage(obj) (and montage(obj, <montagetype>) for any
% canlab_results_fmridisplay layout keyword) composes a MULTI-PANEL display via
% multiview() — the sagittal+axial 'compact' combo by default, matching
% fmri_data.montage. Explicit single-orientation calls ('axial'/'sagittal'/
% 'coronal', slice options, 'existing_axes' — including the per-orientation
% calls multiview/canlab_results_fmridisplay make internally) fall through to the
% single-orientation montage below. The orientation words are deliberately kept
% OUT of the routed set, so montage(obj,'sagittal') stays a single sagittal row.
mv_types = {'compact', 'compact2', 'compact3', 'full', 'full2', 'multirow', ...
'allslices', 'blobcenters', 'regioncenters', 'full hcp', 'full hcp inflated', ...
'full no surfaces', 'hcp grayordinates', 'hcp grayordinates subcortex', ...
'hcp grayordinates compact', 'subcortex compact', 'subcortex full', ...
'subcortex slices', 'subcortex 3d', 'leftright inout', 'leftright inout subcortex', ...
'hcp inflated', 'hcp', 'hcp sphere', 'freesurfer inflated', 'freesurfer white', ...
'freesurfer sphere', 'MNI152NLin2009cAsym white', 'MNI152NLin2009cAsym midthickness', ...
'MNI152NLin2009cAsym pial', 'MNI152NLin6Asym white', 'MNI152NLin6Asym midthickness', ...
'MNI152NLin6Asym pial', 'MNI152NLin6Asym sphere'};

% Keywords that request a SINGLE-orientation montage (existing behaviour) and so
% are NOT routed to multiview: the orientation words, the per-slice options, and
% the existing-axes/figure paths (which the multi-panel layouts use internally,
% preventing recursion). Anything else -> multi-panel multiview (default compact).
single_kw = {'axial', 'coronal', 'sag', 'sagg', 'saggital', 'sagittal', ...
'wh_slice', 'slice_range', 'spacing', 'onerow', 'existing_axes', ...
'volume_data', 'nofigure', 'nofig', 'existing_figure'};
is_single = false;
for i = 1:numel(varargin)
if ischar(varargin{i}) && any(strcmpi(varargin{i}, single_kw)), is_single = true; break; end
end

if ~is_single
mv_type = 'compact'; mv_extra = varargin; % default multi-panel layout
for i = 1:numel(varargin) % use an explicit montagetype if given
if ischar(varargin{i}) && any(strcmpi(varargin{i}, mv_types))
mv_type = varargin{i}; mv_extra(i) = []; break;
end
end
if ~any(strcmpi(varargin, 'noverbose'))
vname = inputname(1); if isempty(vname), vname = 'o2'; end
fprintf('Composing multi-panel display. Equivalent call: %s = multiview(%s, ''%s'');\n', ...
vname, vname, mv_type);
end
obj = multiview(obj, mv_type, mv_extra{:});
dat = []; %#ok<NASGU> % keep the 2nd output defined if requested
return
end

donewfigure = true; % default - new figure and axes. even if donewaxes is on, this can be turned off to use existing figure
Expand Down Expand Up @@ -274,15 +323,20 @@

for i = 1:length(slice_vox_coords)
axes(newax(i));

wh_slice = slice_vox_coords(i);
Z = display_slice(dat, wh_slice, obj.SPACE, myview, lightenstr);

% ✅ Store the actual mm coordinate for this axis
setappdata(newax(i), 'mm_coord', slice_mm_coords(i));

hold on
axis off

% Hide the per-axes interaction toolbar (the "..." overflow button that
% appears in the corner of every slice in recent MATLAB releases) and disable
% the default pan/zoom/rotate interactions, which are meaningless for a slice.
hide_axes_toolbar(newax(i));
end


Expand All @@ -291,6 +345,25 @@

obj.montage{end + 1} = struct('axis_handles', newax, 'orientation', myview, 'slice_mm_coords', slice_mm_coords);

% Pull existing blob layers onto this newly added montage, so blobs that were
% added BEFORE this montage (e.g. drawn onto surfaces first) also appear here.
% Mirrors the surface() pull-in. For the usual montage-then-addblobs order there
% are no layers yet, so this is a no-op. See VISUALIZATION_OVERHAUL_NOTES.md.
new_montage_idx = numel(obj.montage);
for k = 1:numel(obj.activation_maps)
layer = obj.activation_maps{k};
if ~isfield(layer, 'mapdata') || isempty(layer.mapdata), continue, end
render_args = {};
if isfield(layer, 'render_args') && ~isempty(layer.render_args), render_args = layer.render_args; end
[blobhan, this_cmaprange] = render_blobs(layer, obj.montage{new_montage_idx}, obj.SPACE, render_args{:});
if ~isempty(blobhan)
obj.activation_maps{k}.blobhandles = [obj.activation_maps{k}.blobhandles; blobhan];
if ~isfield(obj.activation_maps{k}, 'cmaprange') || isempty(obj.activation_maps{k}.cmaprange)
obj.activation_maps{k}.cmaprange = this_cmaprange;
end
end
end

if brighten_factor % if we are brightening - this is a bit slower...
% set color map to enhance contrast
datvec = dat(:);
Expand Down Expand Up @@ -372,37 +445,49 @@
end

if ~donewaxes
% Break and return here if we are using existing axes.
% was apparently not implemented. now checking for axis
% handles. SG 2016/10/26
% Using existing axes. Ensure we end up with exactly num_axes valid
% axes: use the provided ones if enough were passed, otherwise EXPAND
% from the first provided axis into a row of num_axes axes. The expand
% path is what canlab_results_fmridisplay 'multirow' relies on — it
% passes a single placeholder axis and lets montage lay out the slices
% (previously this was gated on there being >1 axis in the figure, so
% a lone axis with many slices errored: "Index exceeds ... must not
% exceed 1"). SG 2016/10/26; fixed 2026.
valid = false(1, numel(newax));
for k = 1:numel(newax)
axcheck(k) = isscalar(newax(k)) && ishandle(newax(k)) && strcmp(get(newax(k), 'type'), 'axes');
valid(k) = isscalar(newax(k)) && ishandle(newax(k)) && strcmp(get(newax(k), 'type'), 'axes');
end
if numel(newax)>1 && sum(axcheck)==numel(newax)
axis(newax,'off');
newax = newax(valid);
if isempty(newax)
error('fmridisplay:montage:badExistingAxes', ...
'''existing_axes'' did not contain any valid axes handles.');
end

if numel(newax) >= num_axes
newax = newax(1:num_axes);
axis(newax, 'off');
else
axis(newax,'off');
allaxh = findobj(gcf, 'Type', 'axes');
if length(allaxh) > 1
init_pos = get(newax, 'Position');
if num_axes > 1
for i = 1:num_axes
x_pos = init_pos(1) + (i * (0.85 / num_axes));
newax(i) = axes('Position', [x_pos,init_pos(2),init_pos(3),init_pos(4)]);
axis(newax(i), 'image');
end
end
% Fewer axes than slices: expand from the first provided axis.
init_pos = get(newax(1), 'Position');
axis(newax(1), 'off');
for i = 1:num_axes
x_pos = init_pos(1) + (i * (0.85 / num_axes));
newax(i) = axes('Position', [x_pos, init_pos(2), init_pos(3), init_pos(4)]);
axis(newax(i), 'image');
end
end
slices_fig_h = get(newax(1), 'Parent');
while ~isa(slices_fig_h,'matlab.ui.Figure')
slices_fig_h = get(slices_fig_h, 'Parent');
end

slices_fig_h = ancestor(newax(1), 'figure');
return
end

if donewfigure
slices_fig_h = figure; %create_figure(myview);
elseif canlab_is_uifigure(gcf)
% 'nofigure' asked to reuse the current figure, but it is a uifigure
% (the controller): subplot/axes cannot draw into it, so open a fresh
% traditional figure instead (consistent with the other methods).
slices_fig_h = figure;
else
slices_fig_h = gcf;
end
Expand All @@ -429,3 +514,10 @@
end % setup axes

end % main function


function hide_axes_toolbar(ax)
% Hide the per-axes "..." interaction toolbar on the slice axes. Delegates to the
% shared canlab_hide_axes_toolbar so montages and surfaces behave identically.
canlab_hide_axes_toolbar(ax);
end
68 changes: 68 additions & 0 deletions CanlabCore/@fmridisplay/multiview.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
function obj = multiview(obj, montagetype, varargin)
% multiview Compose a canonical multi-panel display onto this fmridisplay object.
%
% Lays out one of the montage/surface figure compositions defined in
% canlab_results_fmridisplay (e.g. 'compact', 'compact2', 'full', 'multirow',
% the HCP / freesurfer surface sets, the subcortical layouts, ...) onto THIS
% object, then renders any existing blob layers onto the new panels (via the
% fmridisplay.montage / surface pull-in). This is the fmridisplay method behind
% the default fmri_data.montage appearance; fmridisplay.montage(obj) routes here
% with 'compact'.
%
% :Usage:
% ::
%
% obj = multiview(obj) % default 'compact' (sag + axial combo)
% obj = multiview(obj, 'full') % montages + surfaces
% obj = multiview(obj, 'compact2')
% obj = multiview(obj, montagetype, ...) % any canlab_results_fmridisplay type
%
% :Inputs:
%
% **obj:**
% An fmridisplay object (handle). Composition is added to it in place.
%
% **montagetype:**
% A canlab_results_fmridisplay montage/surface layout keyword. Default
% 'compact'. See `help canlab_results_fmridisplay` for the full list
% (compact, compact2, compact3, full, full2, multirow, allslices,
% blobcenters/regioncenters, full hcp, hcp inflated, freesurfer inflated,
% subcortex *, ...).
%
% :Optional Inputs:
%
% Any further arguments are passed through to canlab_results_fmridisplay.
%
% :Outputs:
%
% **obj:**
% The same handle, with the composed montages/surfaces registered and
% existing blob layers rendered onto them.
%
% :Examples:
% ::
%
% o2 = fmridisplay;
% o2 = addblobs(o2, region(my_stat_image));
% o2 = multiview(o2, 'full'); % montages + surfaces, blobs pulled in
%
% :See also:
% - canlab_results_fmridisplay, montage, surface, addblobs, fmridisplay
%
% ..
% 2026 visualization overhaul
% ..

if nargin < 2 || isempty(montagetype), montagetype = 'compact'; end

% Delegate the figure/axes composition to canlab_results_fmridisplay, passing
% THIS (handle) object so it composes onto it rather than building a new one
% (see its do_setup_display logic). 'noblobs' skips adding new blobs and
% 'noremove' keeps any EXISTING blob layers on the object (canlab_results_
% fmridisplay otherwise clears them before adding). The existing layers are
% rendered onto the new montages/surfaces by the fmridisplay.montage / surface
% pull-in as the panels are created. fmridisplay is a handle class, so obj is
% updated in place; we also capture the return for clarity.
obj = canlab_results_fmridisplay([], obj, montagetype, 'noblobs', 'noremove', varargin{:});

end
27 changes: 12 additions & 15 deletions CanlabCore/@fmridisplay/removeblobs.m
Original file line number Diff line number Diff line change
Expand Up @@ -34,36 +34,33 @@
% from deleted graphics handles.
obj = prune_dead_views(obj);

to_remove = [];

for i = 1:length(obj.activation_maps)

if isfield(obj.activation_maps{i}, 'blobhandles')

wh = ishandle(obj.activation_maps{i}.blobhandles);

if any(wh)
delete(obj.activation_maps{i}.blobhandles(wh))

to_remove(end+1) = i;
end

if isfield(obj.activation_maps{i}, 'legendhandle')

wh = ishandle(obj.activation_maps{i}.legendhandle);

if any(wh)
delete(obj.activation_maps{i}.legendhandle(wh))
end

end
end

end

% Surfaces
end

obj.activation_maps(to_remove) = [];
% Remove ALL layers. (Previously only layers with live montage blobhandles were
% dropped, which left surface-only layers — whose blobs are vertex colours, not
% graphics handles — stuck in the object and in the controller.) Surface colours
% are erased separately below.
obj.activation_maps = {};

if ~isempty(obj.surface)

Expand Down
Loading
Loading