Skip to content

Commit 86207f5

Browse files
authored
Merge branch 'master' into enh-codespell
2 parents 02aa46c + 767d4b6 commit 86207f5

629 files changed

Lines changed: 48106 additions & 4371 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ classifier_compare.ipynb
1515
*.asv
1616
CanlabCore/Visualization_functions/violinplot.m
1717
CanlabCore/Visualization_functions/violinplot.m
18+
~*

CanlabCore/@atlas/assign_vals.m

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
function [dat, tbl] = assign_vals(atl, varargin)
2+
% ASSIGN_VALS Assigns numeric values to atlas regions and creates an fmri_data object.
3+
%
4+
% [dat, tbl] = assignvals2atlas(atl, 'reg_names', reg_names, 'vals', vals, 'sort', true)
5+
%
6+
% Inputs:
7+
% atl - An atlas structure (with fields like .labels, .data)
8+
% reg_names - Cell array of region names (must match atlas region shorttitles)
9+
% vals - Numeric values (same length as reg_names)
10+
% sort - (Optional) Logical flag to sort output table by value (default = true)
11+
%
12+
% Outputs:
13+
% dat - fmri_data object with assigned region values
14+
% tbl - Table of regions and assigned values
15+
%
16+
% Author: Michael Sun, Ph.D. 4/23/2025
17+
18+
% Parse optional inputs
19+
parser = inputParser;
20+
parser.KeepUnmatched = true;
21+
22+
addParameter(parser, 'reg_names', atl.labels, @(x) iscell(x) || isstring(x));
23+
addParameter(parser, 'vals', repmat(0, numel(atl.labels), 1), @isnumeric);
24+
addParameter(parser, 'sort', true, @islogical);
25+
26+
parse(parser, varargin{:});
27+
reg_names = cellstr(parser.Results.reg_names); % ensure cell array of char
28+
vals = parser.Results.vals;
29+
do_sort = parser.Results.sort;
30+
31+
32+
% Validate input lengths
33+
if numel(reg_names) ~= numel(vals)
34+
error('reg_names and vals must be the same length.');
35+
end
36+
37+
38+
% ------------------------------
39+
% Convert atlas to region object
40+
% ------------------------------
41+
atl_r = atlas2region(atl);
42+
counts = zeros(numel(atl_r), 1);
43+
44+
% ------------------------------
45+
% Assign values to regions
46+
% ------------------------------
47+
for i = 1:numel(atl_r)
48+
idx = find(strcmp(reg_names, atl_r(i).shorttitle));
49+
if ~isempty(idx)
50+
atl_r(i).Z = repmat(vals(idx), 1, size(atl_r(i).Z, 2));
51+
counts(i) = vals(idx);
52+
else
53+
atl_r(i).Z = zeros(1, size(atl_r(i).Z, 2));
54+
counts(i) = 0;
55+
end
56+
end
57+
58+
% ------------------------------
59+
% Create output table
60+
% ------------------------------
61+
region_labels = {atl_r.shorttitle}';
62+
tbl = table(region_labels, counts, 'VariableNames', {'Region', 'Value'});
63+
64+
if do_sort
65+
tbl = sortrows(tbl, 'Value', 'descend');
66+
end
67+
68+
% ------------------------------
69+
% Convert region object back to fmri_data
70+
% ------------------------------
71+
dat = region2fmri_data(atl_r, fmri_data(atl));
72+
73+
end

CanlabCore/@atlas/atlas.m

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,13 @@
150150
atlas_name % a short description or name of the atlas
151151
probability_maps % voxels x regions matrix with probability values for each region
152152
labels
153-
label_descriptions % a regions x 1 cell array of long-form descriptions for labels
153+
label_descriptions = {} % a regions x 1 cell array of long-form descriptions for labels
154154
labels_2
155155
labels_3
156156
labels_4
157157
labels_5
158158
references
159-
space_description = '';
159+
space_description = ''; % set this to something compatable with the allowed_sourcespace from render_on_surface for automatic projections to supported surfaces when plotting.
160160
property_descriptions = { ...
161161
'atlas_name: a short description or name of the atlas' ...
162162
'probability_maps: voxels x regions matrix with probability values for each region' ...
@@ -385,7 +385,7 @@
385385
% Now extract the actual data from the mask
386386
switch spm('Ver')
387387

388-
case {'SPM12','SPM8', 'SPM5'}
388+
case {'SPM25', 'SPM12','SPM8', 'SPM5'}
389389
imgdat = iimg_get_data(maskobj.volInfo, image_names, 'single', verbosestr, 'noexpand');
390390

391391
case {'SPM2', 'SPM99'}

CanlabCore/@atlas/atlas_similarity.m

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,10 +229,11 @@
229229
region_table.Properties.VariableDescriptions{3} = 'Region_Vol_mm: Volume of contiguous region in cubic mm.';
230230
region_table.Properties.VariableDescriptions{4} = 'Atlas_regions_covered: Number of reference atlas regions covered at least 25%% by the region. This relates to whether the region covers multiple reference atlas regions';
231231
region_table.Properties.VariableDescriptions{5} = 'Modal_label: Best reference atlas label, defined as reference region with highest number of in-region voxels. Regions covering >25%% of >5 regions labeled as "Multiple regions"';
232-
region_table.Properties.VariableDescriptions{6} = 'Perc_covered_by_label: Percentage of the region covered by the label.';
233-
region_table.Properties.VariableDescriptions{7} = 'Ref_region_perc: Percentage of the label region within the target region.';
234-
region_table.Properties.VariableDescriptions{8} = 'modal_atlas_index: Index number of label region in reference atlas';
235-
region_table.Properties.VariableDescriptions{9} = 'all_regions_covered: All regions covered >5%% in descending order of importance';
232+
region_table.Properties.VariableDescriptions{6} = 'Modal_label_Description: Description of Labels.';
233+
region_table.Properties.VariableDescriptions{7} = 'Perc_covered_by_label: Percentage of the region covered by the label.';
234+
region_table.Properties.VariableDescriptions{8} = 'Ref_region_perc: Percentage of the label region within the target region.';
235+
region_table.Properties.VariableDescriptions{9} = 'modal_atlas_index: Index number of label region in reference atlas';
236+
region_table.Properties.VariableDescriptions{10} = 'all_regions_covered: All regions covered >5%% in descending order of importance';
236237

237238
% %%%% for compatibility with canlab_print_legend_text
238239
coverage_descrip = {sprintf('For example, if a region is labeled ''TE1a'' and Perc_covered_by_label = 8, Ref_region_perc = 38, and Atlas_regions_covered = 17, this means that 8%%%% of the region''s voxels are labeled TE1a, which is the highest percentage among reference label regions. 38%%%% of the region TE1a is covered by the region. However, the region covers at least 25%%%% of 17 distinct labeled reference regions.\n')};
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
function new_atlas_obj = downsample_parcellation(obj, varargin)
2+
% An atlas_obj has multiple label fields meant to label nested parcellations
3+
% at different levels of granularity. labels is the default which must
4+
% contain unique entries and correspond to the number of unique indices
5+
% in the (voxelwise) parcellation index map, but labels_2 ... labels_5
6+
% may contain non-unique entries corresponding to coarser parcellations.
7+
% This function remaps indices and probability maps to correspond to one
8+
% of these coarser parcellations. It is a lossy operations and all labels
9+
% of finer parcellation than the selected level will be discarded.
10+
%
11+
% Input ::
12+
% obj - an atlas_object. Must have at least one of labels_2 ...
13+
% labels_5 defined
14+
% varargin - either a vector of new labels to use, a string or leave empty.
15+
% If you provide a vector, its length should equal number of
16+
% regions in original atlas, with redundant labels for regions
17+
% you want to combine in the downsampled atlas.
18+
% If you provide a string, it should be one of
19+
% {'labels_2', 'labels_3', 'labels_4', 'labels_5'} which will
20+
% then be used as the downsampling vector.
21+
% If you don't provide anything the contents of labels_2 is used
22+
% by default.
23+
%
24+
% Output ::
25+
% atlas_obj - a new atlas object, reindexed according to labelfield
26+
27+
fprintf('Downsampling %s parcels\n', obj.atlas_name);
28+
29+
concat_desc = false;
30+
31+
if isempty(varargin)
32+
labelfield = 'labels_2';
33+
else
34+
for i = 1:length(varargin)
35+
if ismember(varargin{i},{'labels_2','labels_3','labels_3','labels_4','labels_5'})
36+
labelfield=varargin{i};
37+
elseif strcmp(varargin{i},'concat_label_descriptions')
38+
concat_desc = true;
39+
elseif iscell(varargin{i})
40+
if length(varargin{i}) ~= num_regions(obj)
41+
error('New labels has length %d which does not match input atlas parcel count (%d)',length(varargin{1}), num_regions(obj));
42+
end
43+
labelfield='labels';
44+
obj.labels = varargin{1};
45+
else
46+
error('Unrecognized input datatype');
47+
end
48+
end
49+
end
50+
51+
% input check
52+
valid_lbls = {'labels', 'labels_2', 'labels_3', 'labels_4', 'labels_5'};
53+
if ~ismember(labelfield, valid_lbls)
54+
error('labelfield must be one of {''labels_2'', ''labels_3'', ''labels_4'', ''labels_5''})');
55+
end
56+
57+
% check that labels are ordered with finer labels first
58+
n_unique_lbls = zeros(1,length(valid_lbls));
59+
n_lbls = zeros(1,length(valid_lbls));
60+
for i = 1:length(valid_lbls)
61+
lbl = valid_lbls{i};
62+
n_unique_lbls(i) = length(unique(obj.(lbl)));
63+
n_lbls(i) = length(obj.(lbl));
64+
end
65+
66+
new_ind = find(strcmp(valid_lbls,labelfield));
67+
if n_unique_lbls(new_ind) > n_unique_lbls(1)
68+
error('labelfield %s has %d labels, which is finer grained than labels which has %d unique labels',labelfield, n_unique_lbls(new_ind), n_unique_lbls(1));
69+
end
70+
71+
new_lbl = valid_lbls{new_ind};
72+
% note: we remove any label fields below if they're not of the appropriate
73+
% length to correspond to the original parcellation
74+
%kept_lbl_ind = find((n_unique_lbls <= n_unique_lbls(new_ind)) & ...
75+
% (n_lbls == length(obj.(labelfield))));
76+
77+
% find labels with valid fields
78+
kept_lbl_ind = find((n_lbls == length(obj.(labelfield))));
79+
% keep those with indices that are greater than or equal to the desired label
80+
kept_lbl_ind = kept_lbl_ind(kept_lbl_ind >= new_ind);
81+
82+
rm_lbl_ind = find(~ismember(1:length(valid_lbls), 1:length(kept_lbl_ind)));
83+
84+
% merge high resolution regions
85+
[new_lbl_parcels, lbl_exp, c] = unique(obj.(new_lbl),'stable');
86+
new_parcels = cell(1,length(new_lbl_parcels));
87+
88+
% check that higher order labels are nested
89+
for i = 1:length(lbl_exp) % number of unique labels
90+
this_ind = find(i == c);
91+
for j = 2:length(kept_lbl_ind)
92+
higher_order_lbls = obj.(valid_lbls{kept_lbl_ind(j)});
93+
uniq_ho_lbls = unique(higher_order_lbls(this_ind));
94+
if length(uniq_ho_lbls) > 1
95+
lbls_with_amp = strcat(uniq_ho_lbls,repmat({' & '},1,length(uniq_ho_lbls)));
96+
lbls_with_amp = cat(2,lbls_with_amp{:});
97+
lbls_with_amp = lbls_with_amp(1:end-3); % drop trailing ampersand
98+
obj.(valid_lbls{kept_lbl_ind(j)})(this_ind) = {lbls_with_amp};
99+
100+
warning('Higher order labels are not nested. Merging select entries of %s into %s', valid_lbls{kept_lbl_ind(j)},lbls_with_amp);
101+
end
102+
end
103+
end
104+
105+
106+
% init progress watcher
107+
n_reg = length(new_lbl_parcels);
108+
last_msg = sprintf('Creating new region 1/%d',n_reg);
109+
fprintf('%s',last_msg);
110+
111+
% working with sparse matrices is much faster for atlases with many
112+
% parcels, while atlases with few parcels are likely to run quickly
113+
% regardless
114+
obj.probability_maps = sparse(double(obj.probability_maps));
115+
for i = 1:length(new_lbl_parcels)
116+
% this can be parallelized but it's very memory intensive for some
117+
% atlases (e.g. canlab2023) and parallelization makes the problem worse
118+
% by several factors.
119+
120+
% update progress watcher
121+
delete_last_chars = length(last_msg);
122+
fprintf('%s',char(8*ones(1,delete_last_chars)))
123+
new_msg = sprintf('Creating new region %d/%d',i,n_reg);
124+
fprintf('%s',new_msg);
125+
last_msg = new_msg;
126+
127+
new_parcel{i} = obj.select_atlas_subset(new_lbl_parcels(i), new_lbl, 'exact', 'flatten');
128+
if concat_desc
129+
% append semicolon to all constituent descriptions
130+
label_desc = cellfun(@(x1)[x1, ';'], obj.select_atlas_subset(new_lbl_parcels(i), new_lbl, 'exact').label_descriptions, 'UniformOutput', false);
131+
new_parcel{i}.label_descriptions = {strcat(label_desc{:})};
132+
end
133+
end
134+
fprintf('\n');
135+
136+
% init progress watcher
137+
n_reg = length(new_parcel);
138+
last_msg = sprintf('Merging new region 1/%d',n_reg);
139+
fprintf('%s',last_msg);
140+
141+
% Combine parcels
142+
new_atlas_obj = new_parcel{1};
143+
for i = 2:length(new_parcel)
144+
% update progress watcher
145+
delete_last_chars = length(last_msg);
146+
fprintf('%s',char(8*ones(1,delete_last_chars)))
147+
new_msg = sprintf('Merging new region %d/%d',i,n_reg);
148+
fprintf('%s',new_msg);
149+
last_msg = new_msg;
150+
151+
new_atlas_obj = new_atlas_obj.merge_atlases(new_parcel{i},'noverbose');
152+
end
153+
fprintf('\n');
154+
new_atlas_obj.probability_maps = sparse(new_atlas_obj.probability_maps);
155+
156+
for i = 1:length(kept_lbl_ind)
157+
% increment by 1
158+
old_lbls = obj.(valid_lbls{kept_lbl_ind(i)});
159+
160+
new_atlas_obj.(valid_lbls{i}) = old_lbls(lbl_exp(1:length(lbl_exp)));
161+
end
162+
163+
% delete finer parcellations
164+
for ind = rm_lbl_ind(:)'
165+
new_atlas_obj.(valid_lbls{ind}) = {};
166+
end
167+
168+
new_atlas_obj.references = unique(new_atlas_obj.references,'rows');
169+
170+
end
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
function tbl = get_regions_at_crosshairs(obj, varargin)
2+
coords = spm_orthviews('pos')';
3+
xyz = obj.volInfo.xyzlist;
4+
xyzmm = (obj.volInfo.mat*[xyz, ones(size(xyz,1),1)]')';
5+
dist = sum((coords - xyzmm(:,1:3)).^2,2);
6+
crosshairs_vx_ind = find(dist == min(dist));
7+
8+
obj = obj.replace_empty();
9+
10+
if ~isempty(obj.probability_maps)
11+
p = full(obj.probability_maps(crosshairs_vx_ind,:));
12+
region_ind = find(p>0);
13+
p = p(region_ind);
14+
else
15+
p = 1;
16+
region_ind = obj.dat(crosshairs_vx_ind);
17+
end
18+
19+
if isempty(region_ind)
20+
tbl = table();
21+
return
22+
end
23+
24+
[labels,labels_2,labels_3,labels_4,labels_5,label_descriptions] = deal(cell(length(region_ind),1));
25+
for i = 1:length(region_ind)
26+
labels(i) = obj.labels(region_ind(i));
27+
if length(obj.labels_2) == length(obj.labels)
28+
labels_2(i) = obj.labels_2(region_ind(i));
29+
end
30+
if length(obj.labels_3) == length(obj.labels)
31+
labels_3(i) = obj.labels_3(region_ind(i));
32+
end
33+
if length(obj.labels_4) == length(obj.labels)
34+
labels_4(i) = obj.labels_4(region_ind(i));
35+
end
36+
if length(obj.labels_5) == length(obj.labels)
37+
labels_5(i) = obj.labels_5(region_ind(i));
38+
end
39+
if length(obj.label_descriptions) == length(obj.labels)
40+
label_descriptions(i) = obj.label_descriptions(region_ind(i));
41+
end
42+
end
43+
44+
tbl = table(labels, labels_2, labels_3, labels_4, labels_5, label_descriptions, p(:),...
45+
'VariableNames',{'labels','labels_2','labels_3','labels_4','labels_5','label_descriptions','prob'});
46+
47+
[~,I] = sort(tbl.prob,'descend');
48+
tbl = tbl(I,:);
49+
50+
if ismember(varargin,'display')
51+
spm_orthviews('caption',1,evalc('disp([tbl.labels, arrayfun(@num2str, tbl.prob, ''UniformOutput'', false)])'))
52+
end
53+
end

CanlabCore/@atlas/label_table.m

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function tbl=label_table(atlas_obj)
2+
% LABEL_TABLE Create a table from an atlas object
3+
%
4+
% TBL = LABEL_TABLE(ATLAS_OBJ) takes an atlas object ATLAS_OBJ and
5+
% creates a table TBL containing the label descriptions and various
6+
% labels.
7+
%
8+
% Input:
9+
% - atlas_obj: An object containing atlas data with the following
10+
% properties:
11+
% * label_descriptions: Descriptions of the labels
12+
% * labels: Primary labels
13+
% * labels_2: Secondary labels
14+
% * labels_3: Tertiary labels
15+
% * labels_4: Quaternary labels
16+
% * labels_5: Quinary labels
17+
%
18+
% Output:
19+
% - tbl: A table containing the label descriptions and labels from
20+
% the atlas object, with appropriate variable names.
21+
%
22+
% Example:
23+
% atlas_obj = load_atlas('canlab2024');
24+
% tbl = label_table(atlas_obj);
25+
% disp(tbl);
26+
%
27+
% Michael Sun, Ph.D. 06/03/2024
28+
29+
% Check if the input is valid
30+
if ~strcmp(class(atlas_obj), 'atlas')
31+
error('label_table:InvalidInput', 'Input must be an atlas.');
32+
end
33+
34+
% Check if any of the labels are empty and pad with NaN
35+
fields_to_check = {'labels_2', 'labels_3', 'labels_4', 'labels_5'};
36+
for i = 1:length(fields_to_check)
37+
if ~isempty(atlas_obj.(fields_to_check{i}))
38+
atlas_obj.(fields_to_check{i}) = padwithnan(atlas_obj.(fields_to_check{i}), atlas_obj.labels, 2);
39+
else
40+
atlas_obj.(fields_to_check{i}) = repmat({''}, 1, numel(atlas_obj.labels));
41+
end
42+
end
43+
44+
tbl=array2table([atlas_obj.label_descriptions, atlas_obj.labels', atlas_obj.labels_2', atlas_obj.labels_3', atlas_obj.labels_4', atlas_obj.labels_5'], ...
45+
'VariableNames',{'label_descriptions', 'labels', 'labels_2', 'labels_3', 'labels_4', 'labels_5'});
46+
47+
48+
end

0 commit comments

Comments
 (0)