Skip to content

Commit 169f067

Browse files
torwagerclaude
andcommitted
MATLAB: full expanded feature matrix, color-coded viz, and EFA
The existing readers kept scalar channels only. Add tooling to load ALL ~2,768 feature variables (every vector channel expanded per component), visualize them color-coded by category, and run exploratory factor analysis. New matlab/ functions: - featureInfo label/category table for every expanded variable (class, subclass, level, model, per-class color). - featuresToTable one clip -> wide timetable, vectors expanded. - readAnnotationCorpusFull concatenate the whole corpus -> C.X + C.info + C.T. - plotFeatureMatrix heatmap / class-mean view, color-coded by category. - factorAnalysisCorpus EFA (factoran, varimax) on interpretable features; pairwise-correlation fit handles audio-only NaNs; returns loadings + per-timepoint factor scores + plots. Docs: CONTENTS.md MATLAB table + recipe; walkthrough.m section 8. Verified end-to-end on the 83-clip corpus (28,237 timepoints) via the MATLAB engine. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 572550a commit 169f067

7 files changed

Lines changed: 663 additions & 3 deletions

File tree

docs/CONTENTS.md

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,15 @@ Details: [`../analysis/web/README.md`](../analysis/web/README.md).
145145
| `readAnnotations(path)` | load one `.h5`/folder/JSON → struct (stimulus, time_sec, features) |
146146
| `getFeature(ann, "audio/low_level/mfcc")` | one channel + metadata by hierarchical path |
147147
| `featuresToTimetable(ann)` | scalar channels → timetable on the common grid |
148-
| `readAnnotationCorpus(folder)` | stack the whole corpus → `C.X [timepoints × channels]` |
148+
| `readAnnotationCorpus(folder)` | stack the whole corpus → `C.X [timepoints × channels]` (**scalar** channels only) |
149149
| `annotationMovieViewer(movie, ann)` | play movie with synced annotation time series + marker |
150150
| `analyzeCorpus(C)` | correlation heatmap, PCA, channel + class network graphs |
151151
| `selectStimulusSet(C)` | D-optimal high-variance / low-redundancy segment selection |
152+
| `featureInfo()` | label/category table for **all** expanded variables (class, subclass, level, model, color) |
153+
| `featuresToTable(ann)` | one clip → wide table with **every vector expanded** into per-component columns (~2.7k vars) |
154+
| `readAnnotationCorpusFull(folder)` | stack the whole corpus with vectors expanded → `C.X [timepoints × ~2.7k vars]` + `C.info` |
155+
| `plotFeatureMatrix(C)` | heatmap of the full feature time series, color-coded by category |
156+
| `factorAnalysisCorpus(C)` | exploratory factor analysis (EFA / `factoran`) + color-coded loadings plot |
152157

153158
---
154159

@@ -167,14 +172,28 @@ mf = getFeature(ann, "audio/low_level/mfcc"); % a vector channel [n × 13]
167172
m = "data/movies/spacetop/videos/ses-01/ses-01_run-01_order-04_content-parkour.mp4";
168173
annotationMovieViewer(m, "annotations/corpus/ses-01_run-01_order-04_content-parkour")
169174
170-
% 3) Analyze the whole CORPUS
175+
% 3) Analyze the whole CORPUS (scalar channels — correlation / PCA / design tool)
171176
C = readAnnotationCorpus("annotations/corpus");
172177
res = analyzeCorpus(C); % figures → analysis/figures/
173178
sel = selectStimulusSet(C, "K", 20); % design a stimulus set; sel.table
174179
180+
% 4) Load the FULL expanded feature set (~2.7k variables: every vector expanded),
181+
% visualize it color-coded by category, and extract factors with EFA
182+
F = readAnnotationCorpusFull("annotations/corpus"); % F.X [timepoints × ~2768], F.info labels
183+
plotFeatureMatrix(F, "Clip", "BigBuckBunny"); % heatmap, color-coded by class
184+
fa = factorAnalysisCorpus(F, "NumFactors", 10); % EFA + loadings plot; fa.scores = per-timepoint factors
185+
175186
% Python inspection (alternative): h5py / pandas on <id>.h5 and corpus_index.csv
176187
```
177188

189+
> **Two corpus readers.** `readAnnotationCorpus` returns only the **scalar** channels
190+
> (what `analyzeCorpus`/`selectStimulusSet` expect). `readAnnotationCorpusFull` expands
191+
> every **vector** channel (SigLIP/DINOv2/CLAP embeddings, AudioSet/action posteriors,
192+
> EmoNet, MFCC, …) into one column per component — the full ~2,768-variable matrix with a
193+
> companion `featureInfo` label table (class / subclass / level / model / color) for
194+
> color-coded plotting and factor analysis. See the
195+
> [feature map](FEATURE_MAP.md) for how the variables are organized.
196+
178197
```bash
179198
# 4) SEARCH segments by feature in the browser (serve from project root)
180199
python3 tools/serve.py # then open http://localhost:8000/analysis/web/index.html

docs/walkthrough.m

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,28 @@
7878
disp(sel.table) % rank, stimulus, t_start, t_end, dur_s
7979
% sel.objTrace vs sel.randTrace quantifies the gain over random selection.
8080

81-
%% 8. Where to go next
81+
%% 8. FULL feature set: all ~2,768 variables, color-coded, + factor analysis
82+
% readAnnotationCorpus (above) keeps only scalar channels. To get EVERY variable —
83+
% each vector channel (SigLIP/DINOv2/CLAP embeddings, AudioSet/action posteriors,
84+
% EmoNet, MFCC, ...) expanded into one column per component — use the FULL reader.
85+
F = readAnnotationCorpusFull("annotations/corpus"); % F.X [timepoints x ~2768]
86+
% F.info is the label table: one row per column, with class/subclass/level/model/color.
87+
fprintf("full set: %d variables (%d embedding, %d interpretable) over %d timepoints\n", ...
88+
size(F.X,2), sum(F.info.IsEmbedding), sum(~F.info.IsEmbedding), size(F.X,1));
89+
90+
% Visualize the whole feature matrix for one clip, color-coded by category:
91+
plotFeatureMatrix(F, "Clip", "BigBuckBunny"); % heatmap; class color strip on the left
92+
% plotFeatureMatrix(F, "Mode","classmean"); % or six class-mean trajectories
93+
94+
% Exploratory factor analysis (interpretable features by default; excludes the opaque
95+
% embeddings and the big AudioSet/Kinetics taxonomies). Returns rotated loadings +
96+
% a per-timepoint factor-score time series you can plot like any feature.
97+
fa = factorAnalysisCorpus(F, "NumFactors", 10); % draws loadings heatmap + scree
98+
r = F.stim == "BigBuckBunny";
99+
figure; plot(F.time_sec(r), fa.scores(r, 1:3)); legend("F1","F2","F3");
100+
xlabel("time (s)"); ylabel("factor score"); title("Factor time series — BigBuckBunny");
101+
102+
%% 9. Where to go next
82103
% - Inspect any other stimulus: change `stimId` in section 0.
83104
% - Annotate a NEW movie (Python):
84105
% PYTHONPATH=src .venv/bin/python -m nfe.run <movie> --vision --audio-hl --events \

matlab/factorAnalysisCorpus.m

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
function F = factorAnalysisCorpus(C, opts)
2+
%FACTORANALYSISCORPUS Exploratory factor analysis of the feature corpus.
3+
% F = FACTORANALYSISCORPUS(C) runs EFA (MATLAB's FACTORAN, maximum-likelihood with
4+
% varimax rotation) on the expanded feature matrix from READANNOTATIONCORPUSFULL
5+
% and returns the factor solution plus a color-coded loadings plot.
6+
%
7+
% By default it analyzes the INTERPRETABLE feature set — it excludes the opaque
8+
% SigLIP/DINOv2/CLAP embeddings and the two large fixed-taxonomy posteriors
9+
% (AudioSet 527 tags, Kinetics 400 actions), which otherwise dominate the solution
10+
% with hundreds of near-collinear columns. Set the options below to include them.
11+
%
12+
% Missing data: features are z-scored, then the factor MODEL is fit from the
13+
% pairwise correlation matrix (using all available data per pair, so audio-only
14+
% clips still contribute their audio/language features) after a nearest
15+
% positive-definite adjustment. Per-timepoint FACTOR SCORES are then computed by
16+
% the regression method with mean-imputation (standardized NaN -> 0), so every
17+
% timepoint gets a score and the factors can be plotted as time series like the
18+
% raw features.
19+
%
20+
% Name-value options:
21+
% "NumFactors" (8) number of factors to extract
22+
% "ExcludeEmbeddings" (true) drop SigLIP/DINOv2/CLAP dimensions
23+
% "ExcludeTaxonomies" (true) drop audioset_tags (527) + action_posteriors (400)
24+
% "Rotate" ("varimax") any FACTORAN rotation ("none","promax",...)
25+
% "MaxNaNFrac" (0.8) drop variables missing in > this fraction of rows
26+
% (0.8 keeps visual features, which are absent on the
27+
% ~67% of timepoints that come from audio-only clips)
28+
% "Plot" (true) draw the loadings heatmap + scree
29+
% "SavePng" ("") export the loadings figure to this path
30+
%
31+
% Returns struct F with:
32+
% F.loadings [p x m] rotated factor loadings
33+
% F.vars [p x k] FEATUREINFO subset for the retained variables (row i <-> loadings row i)
34+
% F.scores [T x m] per-timepoint factor scores (regression method)
35+
% F.specificVar[p x 1] uniquenesses; F.stats FACTORAN stats struct
36+
% F.eigs eigenvalues of the correlation matrix (for the scree plot)
37+
% F.varExplained [1 x m] % variance each rotated factor accounts for
38+
% F.m, F.rotation, F.nEff
39+
%
40+
% Example:
41+
% C = readAnnotationCorpusFull("annotations/corpus");
42+
% F = factorAnalysisCorpus(C, "NumFactors", 8);
43+
% % plot factor-3 time series for one clip:
44+
% r = C.stim=="BigBuckBunny"; plot(C.time_sec(r), F.scores(r,3));
45+
%
46+
% See also READANNOTATIONCORPUSFULL, PLOTFEATUREMATRIX, FACTORAN, PCA.
47+
48+
arguments
49+
C (1,1) struct
50+
opts.NumFactors (1,1) double {mustBePositive, mustBeInteger} = 8
51+
opts.ExcludeEmbeddings (1,1) logical = true
52+
opts.ExcludeTaxonomies (1,1) logical = true
53+
opts.Rotate (1,1) string = "varimax"
54+
opts.MaxNaNFrac (1,1) double = 0.8
55+
opts.Plot (1,1) logical = true
56+
opts.SavePng (1,1) string = ""
57+
end
58+
59+
info = C.info; X = C.X;
60+
61+
% ---- column selection ----
62+
keep = info.Numeric;
63+
if opts.ExcludeEmbeddings, keep = keep & ~info.IsEmbedding; end
64+
if opts.ExcludeTaxonomies
65+
keep = keep & ~ismember(info.Leaf, ["audioset_tags", "action_posteriors"]);
66+
end
67+
keep = keep & (mean(isnan(X),1)' <= opts.MaxNaNFrac); % not-too-missing
68+
X = X(:, keep); info = info(keep, :);
69+
70+
% z-score; drop zero-variance columns
71+
mu = mean(X,1,"omitnan"); sd = std(X,0,1,"omitnan");
72+
good = sd > 0 & isfinite(sd);
73+
X = X(:, good); info = info(good,:); mu = mu(good); sd = sd(good);
74+
Z = (X - mu) ./ sd;
75+
76+
% ---- correlation matrix (pairwise) + nearest-PD repair ----
77+
R = corr(Z, "rows", "pairwise");
78+
R(~isfinite(R)) = 0; R(1:size(R,1)+1:end) = 1;
79+
% drop columns still fully collinear (|r|=1 with an earlier column)
80+
[R, info, Z] = dropCollinear(R, info, Z);
81+
R = nearestPD(R);
82+
p = size(R,1);
83+
nEff = size(Z,1);
84+
85+
m = min(opts.NumFactors, p-1);
86+
if m < opts.NumFactors
87+
warning("factorAnalysisCorpus:m", "Reduced NumFactors to %d (only %d variables).", m, p);
88+
end
89+
90+
% ---- fit factor model ----
91+
% factoran needs complete data; audio-only clips make visual columns NaN, so when
92+
% any NaNs are present we fit from the (repaired) correlation matrix instead.
93+
if any(isnan(Z(:)))
94+
[L, psi, ~, stats] = factoran(R, m, "Xtype","covariance", "Nobs",nEff, ...
95+
"Rotate",char(opts.Rotate));
96+
else
97+
[L, psi, ~, stats] = factoran(Z, m, "Xtype","data", "Rotate",char(opts.Rotate), ...
98+
"Maxit",1000);
99+
end
100+
101+
% ---- per-timepoint factor scores (regression method, mean-imputed) ----
102+
Zi = Z; Zi(isnan(Zi)) = 0; % standardized mean-imputation
103+
W = R \ L; % regression weights (p x m)
104+
scores = Zi * W;
105+
106+
% variance explained by each rotated factor (sum of squared loadings / p)
107+
ve = 100 * sum(L.^2, 1) / p;
108+
[ve, si] = sort(ve, "descend"); L = L(:, si); scores = scores(:, si);
109+
110+
F.loadings = L; F.vars = info; F.scores = scores; F.specificVar = psi;
111+
F.stats = stats; F.varExplained = ve; F.m = m; F.rotation = string(opts.Rotate); F.nEff = nEff;
112+
F.eigs = sort(eig(R), "descend");
113+
114+
fprintf("EFA: %d variables, %d factors (%s), n_eff=%d. Variance explained: %s%%\n", ...
115+
p, m, opts.Rotate, nEff, join(compose("%.1f", ve), "/"));
116+
117+
if opts.Plot, plotLoadings(F, opts.SavePng); end
118+
end
119+
120+
% -------------------------------------------------------------------------
121+
function [R, info, Z] = dropCollinear(R, info, Z)
122+
p = size(R,1); keep = true(p,1);
123+
for i = 2:p
124+
if any(abs(R(i, 1:i-1)) > 0.999 & keep(1:i-1)'), keep(i) = false; end
125+
end
126+
R = R(keep,keep); info = info(keep,:); Z = Z(:,keep);
127+
end
128+
129+
function A = nearestPD(A)
130+
A = (A + A')/2;
131+
[V,D] = eig(A); d = diag(D);
132+
d(d < 1e-6) = 1e-6;
133+
A = V*diag(d)*V';
134+
A = (A + A')/2;
135+
s = sqrt(diag(A)); A = A ./ (s*s'); % rescale to unit diagonal (correlation)
136+
A = (A + A')/2;
137+
A(1:size(A,1)+1:end) = 1; % exact unit diagonal
138+
end
139+
140+
function plotLoadings(F, savePng)
141+
fig = figure("Color","w","Position",[100 100 980 760]);
142+
% sort variables by class then by dominant factor for a readable block structure
143+
classes = ["visual","audio","language","social","situation","affect"];
144+
key = double(arrayfun(@(c) find(classes==c,1), F.vars.Class));
145+
[~, dom] = max(abs(F.loadings), [], 2);
146+
[~, ord] = sortrows([key, dom]);
147+
L = F.loadings(ord,:); vinfo = F.vars(ord,:);
148+
149+
ax = axes(fig, "Position",[0.30 0.08 0.60 0.84]);
150+
imagesc(ax, L); set(ax, "CLim",[-1 1]);
151+
colormap(ax, localDiv()); cb = colorbar(ax); cb.Label.String = "loading";
152+
ax.XTick = 1:F.m; ax.XTickLabel = compose("F%d", 1:F.m);
153+
xlabel(ax, sprintf("factors (%s rotation)", F.rotation));
154+
title(ax, sprintf("EFA loadings — %d variables x %d factors", size(L,1), F.m));
155+
% label a subset of rows (too many to show all)
156+
step = max(1, round(size(L,1)/60));
157+
ax.YTick = 1:step:size(L,1);
158+
lab = vinfo.Leaf + string(compose(" [%s]", extractBefore(vinfo.Level+" ",4)));
159+
comp = vinfo.Component; hasC = comp~="";
160+
lab(hasC) = vinfo.Leaf(hasC) + ":" + comp(hasC);
161+
ax.YTickLabel = lab(1:step:end); ax.FontSize = 7;
162+
163+
% class color strip
164+
strip = axes(fig, "Position",[0.27 0.08 0.02 0.84]);
165+
image(strip, reshape(vinfo.Color,[height(vinfo) 1 3]));
166+
strip.XTick=[]; strip.YTick=[]; box(strip,"on"); linkaxes([ax strip],"y");
167+
168+
% scree inset
169+
sc = axes(fig, "Position",[0.06 0.62 0.16 0.30]);
170+
plot(sc, F.eigs(1:min(20,numel(F.eigs))), "-o","MarkerSize",3); grid(sc,"on");
171+
title(sc,"scree"); xlabel(sc,"factor"); ylabel(sc,"eigenvalue"); sc.FontSize=7;
172+
173+
if savePng ~= ""
174+
exportgraphics(fig, savePng, "Resolution",150); fprintf("wrote %s\n", savePng);
175+
end
176+
end
177+
178+
function m = localDiv()
179+
n=256; x=linspace(0,1,n)'; m=[min(1,1.8*x), 1-abs(2*x-1), min(1,1.8*(1-x))];
180+
end

matlab/featureInfo.m

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
function info = featureInfo(templatePath)
2+
%FEATUREINFO Metadata/label table for every expanded feature variable.
3+
% INFO = FEATUREINFO() reads schema/channel_template.json and returns one row
4+
% per *variable* in the fully-expanded feature set: every scalar channel is one
5+
% row, and every vector channel is expanded into one row per component (so the
6+
% 768-D SigLIP embedding contributes 768 rows, EmoNet 20 rows, etc.). This is the
7+
% companion label table for the wide feature matrix built by FEATURESTOTABLE /
8+
% READANNOTATIONCORPUSFULL — INFO row i describes column i of that matrix.
9+
%
10+
% INFO = FEATUREINFO(TEMPLATEPATH) uses a specific channel template.
11+
%
12+
% Columns of INFO:
13+
% VarName valid, unique MATLAB name used as the table column name
14+
% Path hierarchical channel path (e.g. "affect/depicted/emonet")
15+
% Class top-level class (visual/audio/language/social/situation/affect)
16+
% Subclass second level (e.g. "high_level_static"), or "(direct)"
17+
% Leaf channel leaf name (e.g. "emonet")
18+
% Component component label for a vector element (e.g. "Fear"), else ""
19+
% CompIndex 1-based index within the channel (1 for scalars)
20+
% Model model/tool that produced it
21+
% Dtype scalar | bool | event | vector | categorical | label
22+
% Level "low" | "mid" | "high" (perceptual-abstraction level)
23+
% Numeric true if it enters the numeric matrix (scalar/bool/event/vector)
24+
% IsEmbedding true for the opaque SigLIP/DINOv2/CLAP embedding dimensions
25+
% Color 1x3 RGB for the class (for color-coded plots)
26+
%
27+
% Only Numeric==true rows appear in the numeric matrix X; categorical "*_top"
28+
% codes and free-text label channels (asr_text, scene_description, ...) are listed
29+
% here for reference but excluded from X (their codes are not magnitudes).
30+
%
31+
% See also FEATURESTOTABLE, READANNOTATIONCORPUSFULL, PLOTFEATUREMATRIX.
32+
33+
arguments
34+
templatePath (1,1) string = fullfile(fileparts(fileparts(mfilename("fullpath"))), ...
35+
"schema", "channel_template.json")
36+
end
37+
38+
t = jsondecode(fileread(templatePath));
39+
chans = t.channels;
40+
if ~iscell(chans), chans = num2cell(chans); end % normalize to cell of structs
41+
42+
Path=strings(0,1); Class=strings(0,1); Subclass=strings(0,1); Leaf=strings(0,1);
43+
Component=strings(0,1); CompIndex=zeros(0,1); Model=strings(0,1); Dtype=strings(0,1);
44+
Level=strings(0,1); Numeric=false(0,1); IsEmbedding=false(0,1);
45+
46+
for i = 1:numel(chans)
47+
c = chans{i};
48+
p = string(c.path);
49+
parts = split(p, "/");
50+
cls = parts(1);
51+
sub = "(direct)"; if numel(parts) > 2, sub = parts(2); end
52+
leaf = parts(end);
53+
dt = string(c.dtype);
54+
isNum = ismember(dt, ["scalar", "bool", "event", "vector"]);
55+
isEmb = ismember(leaf, ["siglip_embedding", "dino_embedding", "clap_embedding"]);
56+
lev = localLevel(cls, sub, leaf);
57+
mdl = ""; if isfield(c, "model") && ~isempty(c.model), mdl = string(c.model); end
58+
59+
if dt == "vector"
60+
comps = c.components;
61+
D = double(c.dim);
62+
if isempty(comps), comps = arrayfun(@(k) "d"+string(k-1), (1:D)'); end
63+
for k = 1:D
64+
comp = string(comps{k});
65+
Path(end+1,1)=p; Class(end+1,1)=cls; Subclass(end+1,1)=sub; Leaf(end+1,1)=leaf; %#ok<*AGROW>
66+
Component(end+1,1)=comp; CompIndex(end+1,1)=k; Model(end+1,1)=mdl;
67+
Dtype(end+1,1)=dt; Level(end+1,1)=lev; Numeric(end+1,1)=isNum; IsEmbedding(end+1,1)=isEmb;
68+
end
69+
else
70+
Path(end+1,1)=p; Class(end+1,1)=cls; Subclass(end+1,1)=sub; Leaf(end+1,1)=leaf;
71+
Component(end+1,1)=""; CompIndex(end+1,1)=1; Model(end+1,1)=mdl;
72+
Dtype(end+1,1)=dt; Level(end+1,1)=lev; Numeric(end+1,1)=isNum; IsEmbedding(end+1,1)=isEmb;
73+
end
74+
end
75+
76+
info = table(Path, Class, Subclass, Leaf, Component, CompIndex, Model, Dtype, ...
77+
Level, Numeric, IsEmbedding);
78+
79+
% Stable, unique, valid column names: base = path with "/"->"__"; components get a
80+
% "__<comp>" (or "__d<k-1>" for embeddings) suffix.
81+
base = replace(info.Path, "/", "__");
82+
suffix = strings(height(info), 1);
83+
isVec = info.Dtype == "vector";
84+
suffix(isVec & info.IsEmbedding) = "__d" + string(info.CompIndex(isVec & info.IsEmbedding) - 1);
85+
mask = isVec & ~info.IsEmbedding;
86+
suffix(mask) = "__" + info.Component(mask);
87+
raw = base + suffix;
88+
vn = matlab.lang.makeValidName(raw);
89+
vn = matlab.lang.makeUniqueStrings(vn, {}, namelengthmax);
90+
info.VarName = vn;
91+
92+
% Per-class color (matches analysis/figures/feature_map.svg).
93+
info.Color = localColor(info.Class);
94+
95+
info = movevars(info, "VarName", "Before", "Path");
96+
end
97+
98+
% -------------------------------------------------------------------------
99+
function lev = localLevel(cls, sub, leaf)
100+
% Perceptual-abstraction level per channel; mirrors the FEATURE_MAP summary table.
101+
lev = "high"; % default (high-level semantics)
102+
switch cls
103+
case "visual"
104+
if ismember(sub, ["low_level_static", "dynamic_motion"])
105+
lev = "low";
106+
elseif ismember(sub, ["saliency_aesthetics_depth", "faces_bodies_gaze"])
107+
lev = "mid";
108+
end % else high (high_level_static, action)
109+
case "audio"
110+
if sub == "low_level"
111+
lev = "low";
112+
elseif leaf == "speech_present" || leaf == "word_rate"
113+
lev = "mid";
114+
end % else high (high_level, voice_*)
115+
case "language"
116+
if ismember(leaf, ["freq_zipf", "word_length"])
117+
lev = "low";
118+
elseif sub == "syntax" || ismember(leaf, ["valence","arousal","dominance","concreteness","aoa"])
119+
lev = "mid";
120+
end % else high (surprisal, entropy)
121+
case "social"
122+
if ismember(leaf, ["n_agents", "min_pair_distance"]), lev = "mid"; end
123+
case "affect"
124+
if ismember(leaf, ["face_emotion", "face_valence", "face_arousal"]), lev = "mid"; end
125+
end
126+
end
127+
128+
% -------------------------------------------------------------------------
129+
function C = localColor(classCol)
130+
map = ["visual", "#6366f1"; "audio", "#06b6d4"; "language", "#f59e0b"; ...
131+
"social", "#ec4899"; "situation", "#10b981"; "affect", "#ef4444"];
132+
C = zeros(numel(classCol), 3);
133+
for i = 1:numel(classCol)
134+
j = find(map(:,1) == classCol(i), 1);
135+
hex = "#808080"; if ~isempty(j), hex = map(j,2); end
136+
C(i,:) = double([hex2dec(extractBetween(hex,2,3)), hex2dec(extractBetween(hex,4,5)), ...
137+
hex2dec(extractBetween(hex,6,7))]) / 255;
138+
end
139+
end

0 commit comments

Comments
 (0)