cbrowser 18.77.0 → 18.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/visual/fixation-corpus.d.ts +184 -0
- package/dist/visual/fixation-corpus.d.ts.map +1 -0
- package/dist/visual/fixation-corpus.js +307 -0
- package/dist/visual/fixation-corpus.js.map +1 -0
- package/dist/visual/saliency-metrics.d.ts +111 -0
- package/dist/visual/saliency-metrics.d.ts.map +1 -0
- package/dist/visual/saliency-metrics.js +225 -0
- package/dist/visual/saliency-metrics.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixation corpora — human eye-tracking ground truth, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* WHAT A CORPUS IS FOR HERE
|
|
5
|
+
*
|
|
6
|
+
* Three distinct uses, and conflating them is how saliency work goes wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. VALIDATION — score the shipped model against human fixations. This is
|
|
9
|
+
* what makes an accuracy claim sayable out loud. Study 1 did this against
|
|
10
|
+
* UEyes and reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216.
|
|
11
|
+
* 2. CALIBRATION — tune model parameters (channel weights, surround radius,
|
|
12
|
+
* cell size) to maximise those metrics. Legitimate, but it spends the
|
|
13
|
+
* corpus: a model tuned on a set can no longer be honestly validated on
|
|
14
|
+
* that same set, which is why `split` exists below.
|
|
15
|
+
* 3. PRIORS — derive empirical structure from the corpus and bake it into the
|
|
16
|
+
* model. A centre-bias prior alone lifts AUC substantially on almost any
|
|
17
|
+
* dataset, because humans fixate the middle regardless of content.
|
|
18
|
+
*
|
|
19
|
+
* The interface is deliberately corpus-agnostic. UEyes is what is on disk
|
|
20
|
+
* today; MIT1003, CAT2000 and SALICON have the same shape (images + fixation
|
|
21
|
+
* points + density maps) and should not need new plumbing.
|
|
22
|
+
*
|
|
23
|
+
* WHAT UEYES GIVES THAT MOST CORPORA DO NOT
|
|
24
|
+
*
|
|
25
|
+
* - It is WEB PAGES (1980 of them), not natural scenes. Saliency models tuned on
|
|
26
|
+
* photographs transfer poorly to interfaces, where text and affordances
|
|
27
|
+
* dominate.
|
|
28
|
+
* - Fixation maps at 1s / 3s / 7s separately. Attention at first impression is
|
|
29
|
+
* not attention after consideration, and cbrowser's personas differ precisely
|
|
30
|
+
* on patience — an impatient persona should be scored against 1s, a thorough
|
|
31
|
+
* one against 7s. Scoring every persona against one horizon throws that away.
|
|
32
|
+
* - Per-fixation logs with duration and saccade magnitude/direction, so scanpath
|
|
33
|
+
* ORDER is available, not just aggregate density.
|
|
34
|
+
*
|
|
35
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
36
|
+
* @license MIT
|
|
37
|
+
*/
|
|
38
|
+
import type { Fixation, SaliencyGrid } from "./saliency-metrics.js";
|
|
39
|
+
/** Viewing durations UEyes separates. Others may expose only one. */
|
|
40
|
+
export type FixationHorizon = "1s" | "3s" | "7s";
|
|
41
|
+
export interface CorpusStimulus {
|
|
42
|
+
/** Stable id — the image filename without extension. */
|
|
43
|
+
id: string;
|
|
44
|
+
/** Absolute path to the stimulus image. */
|
|
45
|
+
imagePath: string;
|
|
46
|
+
/** Ground-truth fixation density map per horizon, when the corpus has them. */
|
|
47
|
+
fixationMaps: Partial<Record<FixationHorizon, string>>;
|
|
48
|
+
/**
|
|
49
|
+
* Per-population maps, when the corpus separates groups.
|
|
50
|
+
*
|
|
51
|
+
* This is the field that makes a persona model testable. Without it every
|
|
52
|
+
* persona is scored against one undifferentiated ground truth and is
|
|
53
|
+
* penalised for deviating from the average viewer — which is what a persona
|
|
54
|
+
* model is FOR.
|
|
55
|
+
*/
|
|
56
|
+
groupMaps?: Record<string, string>;
|
|
57
|
+
/** Per-population raw scanpath files, when available. */
|
|
58
|
+
scanpaths?: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
export interface CorpusInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
root: string;
|
|
63
|
+
stimuli: CorpusStimulus[];
|
|
64
|
+
horizons: FixationHorizon[];
|
|
65
|
+
/** Absent when the corpus ships only density maps and no raw logs. */
|
|
66
|
+
fixationLogDir?: string;
|
|
67
|
+
citation: string;
|
|
68
|
+
/** Set when the corpus's licence is unresolved. Blocks shipping derived results. */
|
|
69
|
+
licenceHold?: string;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* One participant fixation as recorded by the tracker.
|
|
73
|
+
*
|
|
74
|
+
* Coordinates are NORMALIZED 0-1 in the source logs and stay that way here.
|
|
75
|
+
* Converting to pixels requires the stimulus dimensions, and doing it at load
|
|
76
|
+
* time — before the caller says which grid they are scoring against — is how
|
|
77
|
+
* coordinate-space bugs get baked in. `fixationsToGrid` does the conversion at
|
|
78
|
+
* the point the target size is actually known.
|
|
79
|
+
*/
|
|
80
|
+
export interface RawFixation {
|
|
81
|
+
/** 0-1, left to right. */
|
|
82
|
+
xNorm: number;
|
|
83
|
+
/** 0-1, top to bottom. */
|
|
84
|
+
yNorm: number;
|
|
85
|
+
/** Dwell in seconds. */
|
|
86
|
+
duration: number;
|
|
87
|
+
/** Seconds since stimulus onset — this is what makes horizon filtering possible. */
|
|
88
|
+
startTime: number;
|
|
89
|
+
participantId: string;
|
|
90
|
+
stimulusId: string;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Discover a UEyes-layout corpus.
|
|
94
|
+
*
|
|
95
|
+
* Returns null rather than throwing when the directory is absent: the corpus is
|
|
96
|
+
* 13GB and is not, and should not be, a dependency of the shipped package.
|
|
97
|
+
* Anything that needs it degrades to "not available" rather than failing.
|
|
98
|
+
*/
|
|
99
|
+
export declare function loadUEyesCorpus(root: string): CorpusInfo | null;
|
|
100
|
+
/**
|
|
101
|
+
* Parse one Gazepoint-format tracker log.
|
|
102
|
+
*
|
|
103
|
+
* The columns that matter: MEDIA_NAME (which stimulus), FPOGX/FPOGY (gaze point
|
|
104
|
+
* of regard, normalized), FPOGD (duration), FPOGS (start time), FPOGV (valid).
|
|
105
|
+
*
|
|
106
|
+
* INVALID SAMPLES ARE DROPPED, not clamped. FPOGV=0 means the tracker lost the
|
|
107
|
+
* eye — a blink, a look away, a calibration slip. Those rows still carry
|
|
108
|
+
* coordinates, and treating them as fixations manufactures ground truth from
|
|
109
|
+
* noise. Off-screen coordinates are dropped for the same reason.
|
|
110
|
+
*/
|
|
111
|
+
export declare function parseGazepointLog(logPath: string): RawFixation[];
|
|
112
|
+
/**
|
|
113
|
+
* Keep only fixations that occurred within `seconds` of stimulus onset.
|
|
114
|
+
*
|
|
115
|
+
* This is what lets a persona be scored against the horizon that matches it: an
|
|
116
|
+
* impatient persona compared to 1s of human viewing, a thorough one to 7s.
|
|
117
|
+
* Comparing every persona to the same aggregate map discards the one dimension
|
|
118
|
+
* this corpus was built to expose.
|
|
119
|
+
*/
|
|
120
|
+
export declare function filterByHorizon(fixations: RawFixation[], seconds: number): RawFixation[];
|
|
121
|
+
/** Convert normalized fixations to a grid's pixel space. */
|
|
122
|
+
export declare function toGridFixations(fixations: RawFixation[], gridWidth: number, gridHeight: number): Fixation[];
|
|
123
|
+
/**
|
|
124
|
+
* Build a fixation density map by accumulating Gaussian blobs.
|
|
125
|
+
*
|
|
126
|
+
* Ground-truth maps ship with UEyes, but a corpus that provides only raw logs
|
|
127
|
+
* needs this to score CC at all. Sigma is in grid cells; the literature
|
|
128
|
+
* convention is roughly one degree of visual angle, and for a 32x32 analysis
|
|
129
|
+
* grid that lands near 1.
|
|
130
|
+
*/
|
|
131
|
+
export declare function fixationsToGrid(fixations: Fixation[], width: number, height: number, sigma?: number): SaliencyGrid;
|
|
132
|
+
/**
|
|
133
|
+
* Load a Saliency4ASD-layout corpus (Duan et al. MMSys 2019 / Gutierrez et al.
|
|
134
|
+
* SPIC 2021): 300 natural-scene images from MIT1003, with fixation maps
|
|
135
|
+
* collected SEPARATELY from 14 children with ASD and 14 typically-developing
|
|
136
|
+
* controls.
|
|
137
|
+
*
|
|
138
|
+
* Separate per-group maps are what make this corpus different in kind. Every
|
|
139
|
+
* other corpus here gives one aggregate ground truth, so a persona model can
|
|
140
|
+
* only be scored on how well it predicts an undifferentiated population — which
|
|
141
|
+
* penalises it for the deviation that is its entire purpose. Two groups permit
|
|
142
|
+
* the crossover question instead: does the ASD model beat the neurotypical model
|
|
143
|
+
* on ASD viewers AND lose to it on TD viewers? Only that interaction
|
|
144
|
+
* distinguishes "models this population" from "is simply a worse model".
|
|
145
|
+
*
|
|
146
|
+
* DATA IMBALANCE, from the dataset README: for some images fewer than 14 ASD
|
|
147
|
+
* subjects contributed, because some children looked away from the screen
|
|
148
|
+
* entirely. Those maps are built from fewer fixations and are correspondingly
|
|
149
|
+
* noisier, so `asdSubjectCount` is surfaced per stimulus — an unweighted mean
|
|
150
|
+
* across images silently treats a 3-subject map as equal evidence to a
|
|
151
|
+
* 14-subject one.
|
|
152
|
+
*
|
|
153
|
+
* LICENCE, unresolved as of 2026-07-29: the Zenodo record says CC BY 4.0 while
|
|
154
|
+
* the deposit's own UsageAgreement.txt forbids commercial use. Held outside the
|
|
155
|
+
* repository pending written clarification from the depositors. Nothing derived
|
|
156
|
+
* from it may ship until that is answered.
|
|
157
|
+
*/
|
|
158
|
+
export declare function loadSaliency4ASDCorpus(root: string): CorpusInfo | null;
|
|
159
|
+
/**
|
|
160
|
+
* Parse a Saliency4ASD scanpath file: `Idx, x, y, duration`, pixel coordinates
|
|
161
|
+
* in the stimulus's own space, concatenated across subjects.
|
|
162
|
+
*
|
|
163
|
+
* Subject boundaries are marked by Idx resetting to 0, which is the only signal
|
|
164
|
+
* the format gives. Counting subjects matters because of the imbalance the
|
|
165
|
+
* README warns about.
|
|
166
|
+
*/
|
|
167
|
+
export declare function parseSaliency4ASDScanpath(filePath: string): {
|
|
168
|
+
fixations: Fixation[];
|
|
169
|
+
subjectCount: number;
|
|
170
|
+
};
|
|
171
|
+
/**
|
|
172
|
+
* Deterministic train/test split.
|
|
173
|
+
*
|
|
174
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
175
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
176
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
177
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
178
|
+
* runs and machines without storing a manifest.
|
|
179
|
+
*/
|
|
180
|
+
export declare function splitCorpus(corpus: CorpusInfo, testFraction?: number): {
|
|
181
|
+
train: CorpusStimulus[];
|
|
182
|
+
test: CorpusStimulus[];
|
|
183
|
+
};
|
|
184
|
+
//# sourceMappingURL=fixation-corpus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixation-corpus.d.ts","sourceRoot":"","sources":["../../src/visual/fixation-corpus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAEpE,qEAAqE;AACrE,MAAM,MAAM,eAAe,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAEjD,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;IACX,2CAA2C;IAC3C,SAAS,EAAE,MAAM,CAAC;IAClB,+EAA+E;IAC/E,YAAY,EAAE,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACnC,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,cAAc,EAAE,CAAC;IAC1B,QAAQ,EAAE,eAAe,EAAE,CAAC;IAC5B,sEAAsE;IACtE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,0BAA0B;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,wBAAwB;IACxB,QAAQ,EAAE,MAAM,CAAC;IACjB,oFAAoF;IACpF,SAAS,EAAE,MAAM,CAAC;IAClB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CAgC/D;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CA+ChE;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,WAAW,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,WAAW,EAAE,CAExF;AAED,4DAA4D;AAC5D,wBAAgB,eAAe,CAC7B,SAAS,EAAE,WAAW,EAAE,EACxB,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,GACjB,QAAQ,EAAE,CAMZ;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,SAAS,EAAE,QAAQ,EAAE,EACrB,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,KAAK,SAAM,GACV,YAAY,CAoBd;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,GAAG,IAAI,CA0CtE;AAED;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,MAAM,GACf;IAAE,SAAS,EAAE,QAAQ,EAAE,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAcjD;AAED;;;;;;;;GAQG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,UAAU,EAClB,YAAY,SAAM,GACjB;IAAE,KAAK,EAAE,cAAc,EAAE,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAarD"}
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixation corpora — human eye-tracking ground truth, in one shape.
|
|
3
|
+
*
|
|
4
|
+
* WHAT A CORPUS IS FOR HERE
|
|
5
|
+
*
|
|
6
|
+
* Three distinct uses, and conflating them is how saliency work goes wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. VALIDATION — score the shipped model against human fixations. This is
|
|
9
|
+
* what makes an accuracy claim sayable out loud. Study 1 did this against
|
|
10
|
+
* UEyes and reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216.
|
|
11
|
+
* 2. CALIBRATION — tune model parameters (channel weights, surround radius,
|
|
12
|
+
* cell size) to maximise those metrics. Legitimate, but it spends the
|
|
13
|
+
* corpus: a model tuned on a set can no longer be honestly validated on
|
|
14
|
+
* that same set, which is why `split` exists below.
|
|
15
|
+
* 3. PRIORS — derive empirical structure from the corpus and bake it into the
|
|
16
|
+
* model. A centre-bias prior alone lifts AUC substantially on almost any
|
|
17
|
+
* dataset, because humans fixate the middle regardless of content.
|
|
18
|
+
*
|
|
19
|
+
* The interface is deliberately corpus-agnostic. UEyes is what is on disk
|
|
20
|
+
* today; MIT1003, CAT2000 and SALICON have the same shape (images + fixation
|
|
21
|
+
* points + density maps) and should not need new plumbing.
|
|
22
|
+
*
|
|
23
|
+
* WHAT UEYES GIVES THAT MOST CORPORA DO NOT
|
|
24
|
+
*
|
|
25
|
+
* - It is WEB PAGES (1980 of them), not natural scenes. Saliency models tuned on
|
|
26
|
+
* photographs transfer poorly to interfaces, where text and affordances
|
|
27
|
+
* dominate.
|
|
28
|
+
* - Fixation maps at 1s / 3s / 7s separately. Attention at first impression is
|
|
29
|
+
* not attention after consideration, and cbrowser's personas differ precisely
|
|
30
|
+
* on patience — an impatient persona should be scored against 1s, a thorough
|
|
31
|
+
* one against 7s. Scoring every persona against one horizon throws that away.
|
|
32
|
+
* - Per-fixation logs with duration and saccade magnitude/direction, so scanpath
|
|
33
|
+
* ORDER is available, not just aggregate density.
|
|
34
|
+
*
|
|
35
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
36
|
+
* @license MIT
|
|
37
|
+
*/
|
|
38
|
+
import { existsSync, readFileSync, readdirSync } from "fs";
|
|
39
|
+
import { join, basename, extname } from "path";
|
|
40
|
+
/**
|
|
41
|
+
* Discover a UEyes-layout corpus.
|
|
42
|
+
*
|
|
43
|
+
* Returns null rather than throwing when the directory is absent: the corpus is
|
|
44
|
+
* 13GB and is not, and should not be, a dependency of the shipped package.
|
|
45
|
+
* Anything that needs it degrades to "not available" rather than failing.
|
|
46
|
+
*/
|
|
47
|
+
export function loadUEyesCorpus(root) {
|
|
48
|
+
const imagesDir = join(root, "images");
|
|
49
|
+
if (!existsSync(imagesDir))
|
|
50
|
+
return null;
|
|
51
|
+
const horizons = [];
|
|
52
|
+
for (const h of ["1s", "3s", "7s"]) {
|
|
53
|
+
if (existsSync(join(root, "saliency_maps", `fixmaps_${h}`)))
|
|
54
|
+
horizons.push(h);
|
|
55
|
+
}
|
|
56
|
+
const stimuli = [];
|
|
57
|
+
for (const file of readdirSync(imagesDir)) {
|
|
58
|
+
const ext = extname(file).toLowerCase();
|
|
59
|
+
if (ext !== ".jpg" && ext !== ".png" && ext !== ".jpeg")
|
|
60
|
+
continue;
|
|
61
|
+
const id = basename(file, ext);
|
|
62
|
+
const fixationMaps = {};
|
|
63
|
+
for (const h of horizons) {
|
|
64
|
+
const candidate = join(root, "saliency_maps", `fixmaps_${h}`, file);
|
|
65
|
+
if (existsSync(candidate))
|
|
66
|
+
fixationMaps[h] = candidate;
|
|
67
|
+
}
|
|
68
|
+
stimuli.push({ id, imagePath: join(imagesDir, file), fixationMaps });
|
|
69
|
+
}
|
|
70
|
+
const logDir = join(root, "eyetracker_logs");
|
|
71
|
+
return {
|
|
72
|
+
name: "UEyes",
|
|
73
|
+
root,
|
|
74
|
+
stimuli: stimuli.sort((a, b) => a.id.localeCompare(b.id)),
|
|
75
|
+
horizons,
|
|
76
|
+
fixationLogDir: existsSync(logDir) ? logDir : undefined,
|
|
77
|
+
citation: "Jiang et al., UEyes: Understanding Visual Saliency across User Interface Types. CHI 2023.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Parse one Gazepoint-format tracker log.
|
|
82
|
+
*
|
|
83
|
+
* The columns that matter: MEDIA_NAME (which stimulus), FPOGX/FPOGY (gaze point
|
|
84
|
+
* of regard, normalized), FPOGD (duration), FPOGS (start time), FPOGV (valid).
|
|
85
|
+
*
|
|
86
|
+
* INVALID SAMPLES ARE DROPPED, not clamped. FPOGV=0 means the tracker lost the
|
|
87
|
+
* eye — a blink, a look away, a calibration slip. Those rows still carry
|
|
88
|
+
* coordinates, and treating them as fixations manufactures ground truth from
|
|
89
|
+
* noise. Off-screen coordinates are dropped for the same reason.
|
|
90
|
+
*/
|
|
91
|
+
export function parseGazepointLog(logPath) {
|
|
92
|
+
const text = readFileSync(logPath, "utf8");
|
|
93
|
+
const lines = text.split("\n").filter((l) => l.trim().length > 0);
|
|
94
|
+
if (lines.length < 2)
|
|
95
|
+
return [];
|
|
96
|
+
const header = lines[0].split(",").map((h) => h.trim());
|
|
97
|
+
const col = (name) => header.findIndex((h) => h === name || h.startsWith(`${name}(`));
|
|
98
|
+
const iMedia = col("MEDIA_NAME");
|
|
99
|
+
const iX = col("FPOGX");
|
|
100
|
+
const iY = col("FPOGY");
|
|
101
|
+
const iD = col("FPOGD");
|
|
102
|
+
const iS = col("FPOGS");
|
|
103
|
+
const iV = col("FPOGV");
|
|
104
|
+
const iId = col("FPOGID");
|
|
105
|
+
if (iX < 0 || iY < 0)
|
|
106
|
+
return [];
|
|
107
|
+
const participantId = basename(logPath).replace(/_fixations\.csv$/i, "");
|
|
108
|
+
const out = [];
|
|
109
|
+
const seen = new Set();
|
|
110
|
+
for (let i = 1; i < lines.length; i++) {
|
|
111
|
+
const parts = lines[i].split(",");
|
|
112
|
+
if (iV >= 0 && parts[iV]?.trim() !== "1")
|
|
113
|
+
continue; // tracker lost the eye
|
|
114
|
+
const xNorm = Number(parts[iX]);
|
|
115
|
+
const yNorm = Number(parts[iY]);
|
|
116
|
+
if (!Number.isFinite(xNorm) || !Number.isFinite(yNorm))
|
|
117
|
+
continue;
|
|
118
|
+
if (xNorm < 0 || xNorm > 1 || yNorm < 0 || yNorm > 1)
|
|
119
|
+
continue; // off-stimulus
|
|
120
|
+
// One row per SAMPLE, many samples per fixation. Deduplicate on FPOGID so a
|
|
121
|
+
// long fixation counts once — otherwise dwell time silently becomes a vote
|
|
122
|
+
// multiplier and every metric is weighted by how long the tracker sampled.
|
|
123
|
+
const fixId = iId >= 0 ? parts[iId]?.trim() : undefined;
|
|
124
|
+
const media = iMedia >= 0 ? (parts[iMedia] ?? "").trim() : "";
|
|
125
|
+
const key = `${media}|${fixId ?? `${i}`}`;
|
|
126
|
+
if (fixId && seen.has(key))
|
|
127
|
+
continue;
|
|
128
|
+
if (fixId)
|
|
129
|
+
seen.add(key);
|
|
130
|
+
out.push({
|
|
131
|
+
xNorm,
|
|
132
|
+
yNorm,
|
|
133
|
+
duration: iD >= 0 ? Number(parts[iD]) || 0 : 0,
|
|
134
|
+
startTime: iS >= 0 ? Number(parts[iS]) || 0 : 0,
|
|
135
|
+
participantId,
|
|
136
|
+
stimulusId: media ? basename(media, extname(media)) : "",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return out;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Keep only fixations that occurred within `seconds` of stimulus onset.
|
|
143
|
+
*
|
|
144
|
+
* This is what lets a persona be scored against the horizon that matches it: an
|
|
145
|
+
* impatient persona compared to 1s of human viewing, a thorough one to 7s.
|
|
146
|
+
* Comparing every persona to the same aggregate map discards the one dimension
|
|
147
|
+
* this corpus was built to expose.
|
|
148
|
+
*/
|
|
149
|
+
export function filterByHorizon(fixations, seconds) {
|
|
150
|
+
return fixations.filter((f) => f.startTime <= seconds);
|
|
151
|
+
}
|
|
152
|
+
/** Convert normalized fixations to a grid's pixel space. */
|
|
153
|
+
export function toGridFixations(fixations, gridWidth, gridHeight) {
|
|
154
|
+
return fixations.map((f) => ({
|
|
155
|
+
x: f.xNorm * gridWidth,
|
|
156
|
+
y: f.yNorm * gridHeight,
|
|
157
|
+
duration: f.duration * 1000,
|
|
158
|
+
}));
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Build a fixation density map by accumulating Gaussian blobs.
|
|
162
|
+
*
|
|
163
|
+
* Ground-truth maps ship with UEyes, but a corpus that provides only raw logs
|
|
164
|
+
* needs this to score CC at all. Sigma is in grid cells; the literature
|
|
165
|
+
* convention is roughly one degree of visual angle, and for a 32x32 analysis
|
|
166
|
+
* grid that lands near 1.
|
|
167
|
+
*/
|
|
168
|
+
export function fixationsToGrid(fixations, width, height, sigma = 1.0) {
|
|
169
|
+
const values = new Float64Array(width * height);
|
|
170
|
+
const radius = Math.max(1, Math.ceil(sigma * 3));
|
|
171
|
+
const twoSigmaSq = 2 * sigma * sigma;
|
|
172
|
+
for (const f of fixations) {
|
|
173
|
+
const cx = f.x;
|
|
174
|
+
const cy = f.y;
|
|
175
|
+
const x0 = Math.max(0, Math.floor(cx - radius));
|
|
176
|
+
const x1 = Math.min(width - 1, Math.ceil(cx + radius));
|
|
177
|
+
const y0 = Math.max(0, Math.floor(cy - radius));
|
|
178
|
+
const y1 = Math.min(height - 1, Math.ceil(cy + radius));
|
|
179
|
+
for (let y = y0; y <= y1; y++) {
|
|
180
|
+
for (let x = x0; x <= x1; x++) {
|
|
181
|
+
const d2 = (x + 0.5 - cx) ** 2 + (y + 0.5 - cy) ** 2;
|
|
182
|
+
values[y * width + x] += Math.exp(-d2 / twoSigmaSq);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return { values, width, height };
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Load a Saliency4ASD-layout corpus (Duan et al. MMSys 2019 / Gutierrez et al.
|
|
190
|
+
* SPIC 2021): 300 natural-scene images from MIT1003, with fixation maps
|
|
191
|
+
* collected SEPARATELY from 14 children with ASD and 14 typically-developing
|
|
192
|
+
* controls.
|
|
193
|
+
*
|
|
194
|
+
* Separate per-group maps are what make this corpus different in kind. Every
|
|
195
|
+
* other corpus here gives one aggregate ground truth, so a persona model can
|
|
196
|
+
* only be scored on how well it predicts an undifferentiated population — which
|
|
197
|
+
* penalises it for the deviation that is its entire purpose. Two groups permit
|
|
198
|
+
* the crossover question instead: does the ASD model beat the neurotypical model
|
|
199
|
+
* on ASD viewers AND lose to it on TD viewers? Only that interaction
|
|
200
|
+
* distinguishes "models this population" from "is simply a worse model".
|
|
201
|
+
*
|
|
202
|
+
* DATA IMBALANCE, from the dataset README: for some images fewer than 14 ASD
|
|
203
|
+
* subjects contributed, because some children looked away from the screen
|
|
204
|
+
* entirely. Those maps are built from fewer fixations and are correspondingly
|
|
205
|
+
* noisier, so `asdSubjectCount` is surfaced per stimulus — an unweighted mean
|
|
206
|
+
* across images silently treats a 3-subject map as equal evidence to a
|
|
207
|
+
* 14-subject one.
|
|
208
|
+
*
|
|
209
|
+
* LICENCE, unresolved as of 2026-07-29: the Zenodo record says CC BY 4.0 while
|
|
210
|
+
* the deposit's own UsageAgreement.txt forbids commercial use. Held outside the
|
|
211
|
+
* repository pending written clarification from the depositors. Nothing derived
|
|
212
|
+
* from it may ship until that is answered.
|
|
213
|
+
*/
|
|
214
|
+
export function loadSaliency4ASDCorpus(root) {
|
|
215
|
+
const imagesDir = join(root, "Images");
|
|
216
|
+
if (!existsSync(imagesDir))
|
|
217
|
+
return null;
|
|
218
|
+
const stimuli = [];
|
|
219
|
+
for (const file of readdirSync(imagesDir)) {
|
|
220
|
+
const ext = extname(file).toLowerCase();
|
|
221
|
+
if (ext !== ".png" && ext !== ".jpg" && ext !== ".jpeg")
|
|
222
|
+
continue;
|
|
223
|
+
const id = basename(file, ext);
|
|
224
|
+
// Fixmaps are named "<id>_s.png" regardless of the stimulus extension.
|
|
225
|
+
const asdMap = join(root, "ASD_FixMaps", `${id}_s.png`);
|
|
226
|
+
const tdMap = join(root, "TD_FixMaps", `${id}_s.png`);
|
|
227
|
+
const fixationMaps = {};
|
|
228
|
+
// This corpus has no multi-duration split; the single map is recorded under
|
|
229
|
+
// the longest horizon rather than inventing 1s/3s data it does not contain.
|
|
230
|
+
if (existsSync(tdMap))
|
|
231
|
+
fixationMaps["7s"] = tdMap;
|
|
232
|
+
stimuli.push({
|
|
233
|
+
id,
|
|
234
|
+
imagePath: join(imagesDir, file),
|
|
235
|
+
fixationMaps,
|
|
236
|
+
groupMaps: {
|
|
237
|
+
...(existsSync(asdMap) ? { asd: asdMap } : {}),
|
|
238
|
+
...(existsSync(tdMap) ? { td: tdMap } : {}),
|
|
239
|
+
},
|
|
240
|
+
scanpaths: {
|
|
241
|
+
asd: join(root, "ASD", `ASD_scanpath_${id}.txt`),
|
|
242
|
+
td: join(root, "TD", `TD_scanpath_${id}.txt`),
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
return {
|
|
247
|
+
name: "Saliency4ASD",
|
|
248
|
+
root,
|
|
249
|
+
stimuli: stimuli.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true })),
|
|
250
|
+
horizons: ["7s"],
|
|
251
|
+
citation: "Duan et al., A Dataset of Eye Movements for the Children with Autism Spectrum Disorder. ACM MMSys 2019; " +
|
|
252
|
+
"Gutierrez et al., Signal Processing: Image Communication 92:116092, 2021. Images from Judd et al., ICCV 2009 (MIT1003).",
|
|
253
|
+
licenceHold: "Zenodo record says CC BY 4.0; the deposit's UsageAgreement.txt forbids commercial use. Unresolved — do not ship derived results.",
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Parse a Saliency4ASD scanpath file: `Idx, x, y, duration`, pixel coordinates
|
|
258
|
+
* in the stimulus's own space, concatenated across subjects.
|
|
259
|
+
*
|
|
260
|
+
* Subject boundaries are marked by Idx resetting to 0, which is the only signal
|
|
261
|
+
* the format gives. Counting subjects matters because of the imbalance the
|
|
262
|
+
* README warns about.
|
|
263
|
+
*/
|
|
264
|
+
export function parseSaliency4ASDScanpath(filePath) {
|
|
265
|
+
if (!existsSync(filePath))
|
|
266
|
+
return { fixations: [], subjectCount: 0 };
|
|
267
|
+
const lines = readFileSync(filePath, "utf8").split("\n").map((l) => l.trim()).filter(Boolean);
|
|
268
|
+
const fixations = [];
|
|
269
|
+
let subjectCount = 0;
|
|
270
|
+
for (const line of lines) {
|
|
271
|
+
if (/^idx/i.test(line))
|
|
272
|
+
continue;
|
|
273
|
+
const parts = line.split(",").map((v) => Number(v.trim()));
|
|
274
|
+
if (parts.length < 4 || parts.some((v) => !Number.isFinite(v)))
|
|
275
|
+
continue;
|
|
276
|
+
const [idx, x, y, duration] = parts;
|
|
277
|
+
if (idx === 0)
|
|
278
|
+
subjectCount++;
|
|
279
|
+
fixations.push({ x, y, duration });
|
|
280
|
+
}
|
|
281
|
+
return { fixations, subjectCount };
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Deterministic train/test split.
|
|
285
|
+
*
|
|
286
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
287
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
288
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
289
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
290
|
+
* runs and machines without storing a manifest.
|
|
291
|
+
*/
|
|
292
|
+
export function splitCorpus(corpus, testFraction = 0.3) {
|
|
293
|
+
const hash = (s) => {
|
|
294
|
+
let h = 2166136261;
|
|
295
|
+
for (let i = 0; i < s.length; i++) {
|
|
296
|
+
h ^= s.charCodeAt(i);
|
|
297
|
+
h = Math.imul(h, 16777619);
|
|
298
|
+
}
|
|
299
|
+
return (h >>> 0) / 4294967296;
|
|
300
|
+
};
|
|
301
|
+
const train = [];
|
|
302
|
+
const test = [];
|
|
303
|
+
for (const s of corpus.stimuli)
|
|
304
|
+
(hash(s.id) < testFraction ? test : train).push(s);
|
|
305
|
+
return { train, test };
|
|
306
|
+
}
|
|
307
|
+
//# sourceMappingURL=fixation-corpus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fixation-corpus.js","sourceRoot":"","sources":["../../src/visual/fixation-corpus.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saliency evaluation metrics against human fixation ground truth.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS IN src/ AND NOT research/
|
|
5
|
+
*
|
|
6
|
+
* The Study 1 benchmark (UEyes, Jiang et al. CHI 2023 — 495 web pages, 62
|
|
7
|
+
* participants) reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216 for the COT
|
|
8
|
+
* saliency model. Those numbers were produced by a *Python reimplementation*
|
|
9
|
+
* whose own docstring calls it a "vectorized Python port of CBrowser's
|
|
10
|
+
* computeLabSaliency()" — and the port had already drifted from the shipped
|
|
11
|
+
* code on the day the results were written: the TypeScript weights chrominance
|
|
12
|
+
* 3x over luminance (wA = wB = 3.0), the Python summed all three Lab channels
|
|
13
|
+
* unweighted.
|
|
14
|
+
*
|
|
15
|
+
* So the published accuracy claim did not describe the shipped model. Not
|
|
16
|
+
* because anyone was careless — because two implementations of one concept with
|
|
17
|
+
* nothing binding them will always drift, and a number is the last place that
|
|
18
|
+
* shows up.
|
|
19
|
+
*
|
|
20
|
+
* Putting the metrics here means the benchmark scores the code that ships, by
|
|
21
|
+
* construction. There is one saliency implementation and the evaluator calls it.
|
|
22
|
+
*
|
|
23
|
+
* THE METRICS
|
|
24
|
+
*
|
|
25
|
+
* Standard in the saliency literature (Bylinskii et al., "What do different
|
|
26
|
+
* evaluation metrics tell us about saliency models?", TPAMI 2019). They
|
|
27
|
+
* disagree with each other on purpose, and reporting one alone is how models
|
|
28
|
+
* get oversold:
|
|
29
|
+
*
|
|
30
|
+
* - AUC-Judd — location-based, ranks fixated pixels against all others.
|
|
31
|
+
* Invariant to monotonic rescaling, so it says nothing about calibration.
|
|
32
|
+
* Chance = 0.5.
|
|
33
|
+
* - NSS — value-based, mean normalized saliency at fixated points. Sensitive to
|
|
34
|
+
* false positives in a way AUC is not. Chance = 0.
|
|
35
|
+
* - CC — distribution-based, linear correlation with the full fixation map.
|
|
36
|
+
* Penalises both misses and false alarms symmetrically. Range [-1, 1].
|
|
37
|
+
*
|
|
38
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
39
|
+
* @license MIT
|
|
40
|
+
*/
|
|
41
|
+
/** A grid of saliency values. Row-major, any scale. */
|
|
42
|
+
export interface SaliencyGrid {
|
|
43
|
+
values: Float64Array | number[];
|
|
44
|
+
width: number;
|
|
45
|
+
height: number;
|
|
46
|
+
}
|
|
47
|
+
/** A single human fixation, in the grid's coordinate space. */
|
|
48
|
+
export interface Fixation {
|
|
49
|
+
x: number;
|
|
50
|
+
y: number;
|
|
51
|
+
/** Dwell in ms, if known. Unused by these metrics but carried for weighting. */
|
|
52
|
+
duration?: number;
|
|
53
|
+
}
|
|
54
|
+
export interface SaliencyScores {
|
|
55
|
+
/** Ranks fixated pixels against all pixels. Chance 0.5. */
|
|
56
|
+
aucJudd: number;
|
|
57
|
+
/** Mean normalized saliency at fixations. Chance 0. */
|
|
58
|
+
nss: number;
|
|
59
|
+
/** Pearson correlation with the ground-truth map. Range [-1, 1]. */
|
|
60
|
+
cc: number;
|
|
61
|
+
/** How many fixations landed inside the grid and were scored. */
|
|
62
|
+
fixationsScored: number;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Normalized Scanpath Saliency: the mean of the standardized saliency map at
|
|
66
|
+
* fixated locations.
|
|
67
|
+
*
|
|
68
|
+
* Unlike AUC this punishes a map that is salient everywhere — standardizing
|
|
69
|
+
* first means a model cannot buy score by raising the whole map.
|
|
70
|
+
*/
|
|
71
|
+
export declare function computeNSS(grid: SaliencyGrid, fixations: Fixation[]): {
|
|
72
|
+
nss: number;
|
|
73
|
+
scored: number;
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* AUC-Judd: sweep a threshold over the saliency map; at each level the true
|
|
77
|
+
* positive rate is the share of fixations above it and the false positive rate
|
|
78
|
+
* is the share of ALL pixels above it. Area under that curve.
|
|
79
|
+
*
|
|
80
|
+
* Thresholds are the saliency values AT the fixations, which is what makes this
|
|
81
|
+
* Judd's variant rather than Borji's — no random negative sampling, so it is
|
|
82
|
+
* deterministic. That matters here: a metric used to compare model revisions
|
|
83
|
+
* must not itself introduce variance between runs.
|
|
84
|
+
*/
|
|
85
|
+
export declare function computeAUCJudd(grid: SaliencyGrid, fixations: Fixation[]): {
|
|
86
|
+
auc: number;
|
|
87
|
+
scored: number;
|
|
88
|
+
};
|
|
89
|
+
/**
|
|
90
|
+
* Pearson correlation between the predicted map and a ground-truth fixation
|
|
91
|
+
* density map. Both are standardized, so this is scale-invariant.
|
|
92
|
+
*/
|
|
93
|
+
export declare function computeCC(grid: SaliencyGrid, groundTruth: SaliencyGrid): number;
|
|
94
|
+
/**
|
|
95
|
+
* Score a predicted saliency map against human fixations.
|
|
96
|
+
*
|
|
97
|
+
* `groundTruth` (a fixation density map) is optional because CC needs one while
|
|
98
|
+
* AUC and NSS need only the fixation points. Reporting CC as 0 when it was
|
|
99
|
+
* never computable would be indistinguishable from a genuinely uncorrelated
|
|
100
|
+
* model, so it is omitted from the average rather than faked.
|
|
101
|
+
*/
|
|
102
|
+
export declare function scoreSaliency(predicted: SaliencyGrid, fixations: Fixation[], groundTruth?: SaliencyGrid): SaliencyScores;
|
|
103
|
+
/**
|
|
104
|
+
* Resample a grid to a target size by area-averaging.
|
|
105
|
+
*
|
|
106
|
+
* Ground-truth fixation maps and predicted grids rarely share dimensions, and
|
|
107
|
+
* comparing them at different resolutions is the mistake `computeCC` refuses to
|
|
108
|
+
* make silently. This is the explicit way to reconcile them.
|
|
109
|
+
*/
|
|
110
|
+
export declare function resampleGrid(grid: SaliencyGrid, width: number, height: number): SaliencyGrid;
|
|
111
|
+
//# sourceMappingURL=saliency-metrics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saliency-metrics.d.ts","sourceRoot":"","sources":["../../src/visual/saliency-metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AAEH,uDAAuD;AACvD,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,YAAY,GAAG,MAAM,EAAE,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,+DAA+D;AAC/D,MAAM,WAAW,QAAQ;IACvB,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,gFAAgF;IAChF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,2DAA2D;IAC3D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,GAAG,EAAE,MAAM,CAAC;IACZ,oEAAoE;IACpE,EAAE,EAAE,MAAM,CAAC;IACX,iEAAiE;IACjE,eAAe,EAAE,MAAM,CAAC;CACzB;AAmBD;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAYrG;AAED;;;;;;;;;GASG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,YAAY,EAAE,SAAS,EAAE,QAAQ,EAAE,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAgEzG;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,YAAY,EAAE,WAAW,EAAE,YAAY,GAAG,MAAM,CAa/E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,SAAS,EAAE,YAAY,EACvB,SAAS,EAAE,QAAQ,EAAE,EACrB,WAAW,CAAC,EAAE,YAAY,GACzB,cAAc,CAShB;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,YAAY,CAsB5F"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Saliency evaluation metrics against human fixation ground truth.
|
|
3
|
+
*
|
|
4
|
+
* WHY THIS IS IN src/ AND NOT research/
|
|
5
|
+
*
|
|
6
|
+
* The Study 1 benchmark (UEyes, Jiang et al. CHI 2023 — 495 web pages, 62
|
|
7
|
+
* participants) reported AUC-Judd 0.663 / NSS 0.544 / CC 0.216 for the COT
|
|
8
|
+
* saliency model. Those numbers were produced by a *Python reimplementation*
|
|
9
|
+
* whose own docstring calls it a "vectorized Python port of CBrowser's
|
|
10
|
+
* computeLabSaliency()" — and the port had already drifted from the shipped
|
|
11
|
+
* code on the day the results were written: the TypeScript weights chrominance
|
|
12
|
+
* 3x over luminance (wA = wB = 3.0), the Python summed all three Lab channels
|
|
13
|
+
* unweighted.
|
|
14
|
+
*
|
|
15
|
+
* So the published accuracy claim did not describe the shipped model. Not
|
|
16
|
+
* because anyone was careless — because two implementations of one concept with
|
|
17
|
+
* nothing binding them will always drift, and a number is the last place that
|
|
18
|
+
* shows up.
|
|
19
|
+
*
|
|
20
|
+
* Putting the metrics here means the benchmark scores the code that ships, by
|
|
21
|
+
* construction. There is one saliency implementation and the evaluator calls it.
|
|
22
|
+
*
|
|
23
|
+
* THE METRICS
|
|
24
|
+
*
|
|
25
|
+
* Standard in the saliency literature (Bylinskii et al., "What do different
|
|
26
|
+
* evaluation metrics tell us about saliency models?", TPAMI 2019). They
|
|
27
|
+
* disagree with each other on purpose, and reporting one alone is how models
|
|
28
|
+
* get oversold:
|
|
29
|
+
*
|
|
30
|
+
* - AUC-Judd — location-based, ranks fixated pixels against all others.
|
|
31
|
+
* Invariant to monotonic rescaling, so it says nothing about calibration.
|
|
32
|
+
* Chance = 0.5.
|
|
33
|
+
* - NSS — value-based, mean normalized saliency at fixated points. Sensitive to
|
|
34
|
+
* false positives in a way AUC is not. Chance = 0.
|
|
35
|
+
* - CC — distribution-based, linear correlation with the full fixation map.
|
|
36
|
+
* Penalises both misses and false alarms symmetrically. Range [-1, 1].
|
|
37
|
+
*
|
|
38
|
+
* @copyright 2026 Alexandria Eden alexandria.shai.eden@gmail.com https://cbrowser.ai
|
|
39
|
+
* @license MIT
|
|
40
|
+
*/
|
|
41
|
+
/** Normalize to zero mean, unit standard deviation. */
|
|
42
|
+
function standardize(values) {
|
|
43
|
+
const n = values.length;
|
|
44
|
+
const out = new Float64Array(n);
|
|
45
|
+
if (n === 0)
|
|
46
|
+
return out;
|
|
47
|
+
let mean = 0;
|
|
48
|
+
for (let i = 0; i < n; i++)
|
|
49
|
+
mean += values[i];
|
|
50
|
+
mean /= n;
|
|
51
|
+
let variance = 0;
|
|
52
|
+
for (let i = 0; i < n; i++)
|
|
53
|
+
variance += (values[i] - mean) ** 2;
|
|
54
|
+
variance /= n;
|
|
55
|
+
const sd = Math.sqrt(variance);
|
|
56
|
+
if (sd === 0)
|
|
57
|
+
return out; // A flat map carries no signal; NSS is 0, not NaN.
|
|
58
|
+
for (let i = 0; i < n; i++)
|
|
59
|
+
out[i] = (values[i] - mean) / sd;
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Normalized Scanpath Saliency: the mean of the standardized saliency map at
|
|
64
|
+
* fixated locations.
|
|
65
|
+
*
|
|
66
|
+
* Unlike AUC this punishes a map that is salient everywhere — standardizing
|
|
67
|
+
* first means a model cannot buy score by raising the whole map.
|
|
68
|
+
*/
|
|
69
|
+
export function computeNSS(grid, fixations) {
|
|
70
|
+
const z = standardize(grid.values);
|
|
71
|
+
let sum = 0;
|
|
72
|
+
let scored = 0;
|
|
73
|
+
for (const f of fixations) {
|
|
74
|
+
const x = Math.floor(f.x);
|
|
75
|
+
const y = Math.floor(f.y);
|
|
76
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height)
|
|
77
|
+
continue;
|
|
78
|
+
sum += z[y * grid.width + x];
|
|
79
|
+
scored++;
|
|
80
|
+
}
|
|
81
|
+
return { nss: scored > 0 ? sum / scored : 0, scored };
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* AUC-Judd: sweep a threshold over the saliency map; at each level the true
|
|
85
|
+
* positive rate is the share of fixations above it and the false positive rate
|
|
86
|
+
* is the share of ALL pixels above it. Area under that curve.
|
|
87
|
+
*
|
|
88
|
+
* Thresholds are the saliency values AT the fixations, which is what makes this
|
|
89
|
+
* Judd's variant rather than Borji's — no random negative sampling, so it is
|
|
90
|
+
* deterministic. That matters here: a metric used to compare model revisions
|
|
91
|
+
* must not itself introduce variance between runs.
|
|
92
|
+
*/
|
|
93
|
+
export function computeAUCJudd(grid, fixations) {
|
|
94
|
+
const n = grid.values.length;
|
|
95
|
+
const inBounds = [];
|
|
96
|
+
for (const f of fixations) {
|
|
97
|
+
const x = Math.floor(f.x);
|
|
98
|
+
const y = Math.floor(f.y);
|
|
99
|
+
if (x < 0 || y < 0 || x >= grid.width || y >= grid.height)
|
|
100
|
+
continue;
|
|
101
|
+
inBounds.push(grid.values[y * grid.width + x]);
|
|
102
|
+
}
|
|
103
|
+
if (inBounds.length === 0 || n === 0)
|
|
104
|
+
return { auc: 0.5, scored: 0 };
|
|
105
|
+
// Sorted copy of the whole map, so each threshold's false-positive rate is a
|
|
106
|
+
// binary search rather than a scan — O(n log n) once instead of O(n) per
|
|
107
|
+
// fixation, which is the difference between usable and not on 1980 images.
|
|
108
|
+
const sorted = Float64Array.from(grid.values).sort();
|
|
109
|
+
const countAbove = (t) => {
|
|
110
|
+
let lo = 0;
|
|
111
|
+
let hi = sorted.length;
|
|
112
|
+
while (lo < hi) {
|
|
113
|
+
const mid = (lo + hi) >>> 1;
|
|
114
|
+
if (sorted[mid] < t)
|
|
115
|
+
lo = mid + 1;
|
|
116
|
+
else
|
|
117
|
+
hi = mid;
|
|
118
|
+
}
|
|
119
|
+
return sorted.length - lo;
|
|
120
|
+
};
|
|
121
|
+
const nFix = inBounds.length;
|
|
122
|
+
// UNIQUE thresholds, and TPR counted rather than assumed.
|
|
123
|
+
//
|
|
124
|
+
// Using (i+1)/nFix as the true-positive rate assumes every fixation sits at a
|
|
125
|
+
// distinct saliency value. Ties break it badly, and ties are the normal case:
|
|
126
|
+
// on a flat map every threshold is identical, so FPR jumped straight to 1
|
|
127
|
+
// while TPR was still 1/nFix — which scored a flat map 0.25 instead of the
|
|
128
|
+
// 0.5 that "carries no information" must produce, and pinned the WORST
|
|
129
|
+
// possible predictor at exactly 0.5. Both are the same arithmetic slip.
|
|
130
|
+
// Counting both rates at each distinct threshold is Judd's formulation.
|
|
131
|
+
// (2026-07-29)
|
|
132
|
+
const uniqueThresholds = [...new Set(inBounds)].sort((a, b) => b - a);
|
|
133
|
+
const fixSorted = [...inBounds].sort((a, b) => a - b);
|
|
134
|
+
const fixAtOrAbove = (t) => {
|
|
135
|
+
let lo = 0;
|
|
136
|
+
let hi = fixSorted.length;
|
|
137
|
+
while (lo < hi) {
|
|
138
|
+
const mid = (lo + hi) >>> 1;
|
|
139
|
+
if (fixSorted[mid] < t)
|
|
140
|
+
lo = mid + 1;
|
|
141
|
+
else
|
|
142
|
+
hi = mid;
|
|
143
|
+
}
|
|
144
|
+
return fixSorted.length - lo;
|
|
145
|
+
};
|
|
146
|
+
// Curve from (0,0) through each threshold to (1,1), trapezoidal integration.
|
|
147
|
+
let auc = 0;
|
|
148
|
+
let prevFpr = 0;
|
|
149
|
+
let prevTpr = 0;
|
|
150
|
+
for (const t of uniqueThresholds) {
|
|
151
|
+
const tpr = fixAtOrAbove(t) / nFix;
|
|
152
|
+
const fpr = countAbove(t) / n;
|
|
153
|
+
auc += ((fpr - prevFpr) * (tpr + prevTpr)) / 2;
|
|
154
|
+
prevFpr = fpr;
|
|
155
|
+
prevTpr = tpr;
|
|
156
|
+
}
|
|
157
|
+
auc += ((1 - prevFpr) * (1 + prevTpr)) / 2;
|
|
158
|
+
return { auc: Math.min(1, Math.max(0, auc)), scored: nFix };
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Pearson correlation between the predicted map and a ground-truth fixation
|
|
162
|
+
* density map. Both are standardized, so this is scale-invariant.
|
|
163
|
+
*/
|
|
164
|
+
export function computeCC(grid, groundTruth) {
|
|
165
|
+
if (grid.values.length !== groundTruth.values.length) {
|
|
166
|
+
throw new Error(`CC needs matching grids: predicted ${grid.width}x${grid.height} vs ground truth ${groundTruth.width}x${groundTruth.height}. ` +
|
|
167
|
+
`Resample one before comparing — silently reshaping here would compare unrelated regions.`);
|
|
168
|
+
}
|
|
169
|
+
const a = standardize(grid.values);
|
|
170
|
+
const b = standardize(groundTruth.values);
|
|
171
|
+
let num = 0;
|
|
172
|
+
for (let i = 0; i < a.length; i++)
|
|
173
|
+
num += a[i] * b[i];
|
|
174
|
+
// Both are unit-variance, so the denominator is n.
|
|
175
|
+
return a.length > 0 ? num / a.length : 0;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Score a predicted saliency map against human fixations.
|
|
179
|
+
*
|
|
180
|
+
* `groundTruth` (a fixation density map) is optional because CC needs one while
|
|
181
|
+
* AUC and NSS need only the fixation points. Reporting CC as 0 when it was
|
|
182
|
+
* never computable would be indistinguishable from a genuinely uncorrelated
|
|
183
|
+
* model, so it is omitted from the average rather than faked.
|
|
184
|
+
*/
|
|
185
|
+
export function scoreSaliency(predicted, fixations, groundTruth) {
|
|
186
|
+
const { auc, scored } = computeAUCJudd(predicted, fixations);
|
|
187
|
+
const { nss } = computeNSS(predicted, fixations);
|
|
188
|
+
return {
|
|
189
|
+
aucJudd: auc,
|
|
190
|
+
nss,
|
|
191
|
+
cc: groundTruth ? computeCC(predicted, groundTruth) : Number.NaN,
|
|
192
|
+
fixationsScored: scored,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Resample a grid to a target size by area-averaging.
|
|
197
|
+
*
|
|
198
|
+
* Ground-truth fixation maps and predicted grids rarely share dimensions, and
|
|
199
|
+
* comparing them at different resolutions is the mistake `computeCC` refuses to
|
|
200
|
+
* make silently. This is the explicit way to reconcile them.
|
|
201
|
+
*/
|
|
202
|
+
export function resampleGrid(grid, width, height) {
|
|
203
|
+
const out = new Float64Array(width * height);
|
|
204
|
+
const xScale = grid.width / width;
|
|
205
|
+
const yScale = grid.height / height;
|
|
206
|
+
for (let y = 0; y < height; y++) {
|
|
207
|
+
const y0 = Math.floor(y * yScale);
|
|
208
|
+
const y1 = Math.min(grid.height, Math.max(y0 + 1, Math.ceil((y + 1) * yScale)));
|
|
209
|
+
for (let x = 0; x < width; x++) {
|
|
210
|
+
const x0 = Math.floor(x * xScale);
|
|
211
|
+
const x1 = Math.min(grid.width, Math.max(x0 + 1, Math.ceil((x + 1) * xScale)));
|
|
212
|
+
let sum = 0;
|
|
213
|
+
let count = 0;
|
|
214
|
+
for (let yy = y0; yy < y1; yy++) {
|
|
215
|
+
for (let xx = x0; xx < x1; xx++) {
|
|
216
|
+
sum += grid.values[yy * grid.width + xx];
|
|
217
|
+
count++;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
out[y * width + x] = count > 0 ? sum / count : 0;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { values: out, width, height };
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=saliency-metrics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"saliency-metrics.js","sourceRoot":"","sources":["../../src/visual/saliency-metrics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuCG;AA4BH,uDAAuD;AACvD,SAAS,WAAW,CAAC,MAAyB;IAC5C,MAAM,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC;IACxB,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAChC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC;IACxB,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,CAAC;IAC9C,IAAI,IAAI,CAAC,CAAC;IACV,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,QAAQ,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC;IAChE,QAAQ,IAAI,CAAC,CAAC;IACd,MAAM,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC/B,IAAI,EAAE,KAAK,CAAC;QAAE,OAAO,GAAG,CAAC,CAAC,mDAAmD;IAC7E,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;QAAE,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAC7D,OAAO,GAAG,CAAC;AACb,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,IAAkB,EAAE,SAAqB;IAClE,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,MAAM,GAAG,CAAC,CAAC;IACf,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;YAAE,SAAS;QACpE,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;QAC7B,MAAM,EAAE,CAAC;IACX,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;AACxD,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,cAAc,CAAC,IAAkB,EAAE,SAAqB;IACtE,MAAM,CAAC,GAAG,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC;IAC7B,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAC1B,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,IAAI,CAAC,MAAM;YAAE,SAAS;QACpE,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;IACjD,CAAC;IACD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;QAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;IAErE,6EAA6E;IAC7E,yEAAyE;IACzE,2EAA2E;IAC3E,MAAM,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,MAA2B,CAAC,CAAC,IAAI,EAAE,CAAC;IAC1E,MAAM,UAAU,GAAG,CAAC,CAAS,EAAU,EAAE;QACvC,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAI,EAAE,GAAG,MAAM,CAAC,MAAM,CAAC;QACvB,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;;gBAC7B,EAAE,GAAG,GAAG,CAAC;QAChB,CAAC;QACD,OAAO,MAAM,CAAC,MAAM,GAAG,EAAE,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,IAAI,GAAG,QAAQ,CAAC,MAAM,CAAC;IAE7B,0DAA0D;IAC1D,EAAE;IACF,8EAA8E;IAC9E,8EAA8E;IAC9E,0EAA0E;IAC1E,2EAA2E;IAC3E,uEAAuE;IACvE,wEAAwE;IACxE,wEAAwE;IACxE,eAAe;IACf,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtE,MAAM,SAAS,GAAG,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,MAAM,YAAY,GAAG,CAAC,CAAS,EAAU,EAAE;QACzC,IAAI,EAAE,GAAG,CAAC,CAAC;QACX,IAAI,EAAE,GAAG,SAAS,CAAC,MAAM,CAAC;QAC1B,OAAO,EAAE,GAAG,EAAE,EAAE,CAAC;YACf,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC;YAC5B,IAAI,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC;gBAAE,EAAE,GAAG,GAAG,GAAG,CAAC,CAAC;;gBAChC,EAAE,GAAG,GAAG,CAAC;QAChB,CAAC;QACD,OAAO,SAAS,CAAC,MAAM,GAAG,EAAE,CAAC;IAC/B,CAAC,CAAC;IAEF,6EAA6E;IAC7E,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,gBAAgB,EAAE,CAAC;QACjC,MAAM,GAAG,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC;QACnC,MAAM,GAAG,GAAG,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC9B,GAAG,IAAI,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;QAC/C,OAAO,GAAG,GAAG,CAAC;QACd,OAAO,GAAG,GAAG,CAAC;IAChB,CAAC;IACD,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC;IAC3C,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAC9D,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,SAAS,CAAC,IAAkB,EAAE,WAAyB;IACrE,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,KAAK,WAAW,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;QACrD,MAAM,IAAI,KAAK,CACb,sCAAsC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,MAAM,oBAAoB,WAAW,CAAC,KAAK,IAAI,WAAW,CAAC,MAAM,IAAI;YAC5H,0FAA0F,CAC7F,CAAC;IACJ,CAAC;IACD,MAAM,CAAC,GAAG,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACtD,mDAAmD;IACnD,OAAO,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;AAC3C,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAC3B,SAAuB,EACvB,SAAqB,EACrB,WAA0B;IAE1B,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,cAAc,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IAC7D,MAAM,EAAE,GAAG,EAAE,GAAG,UAAU,CAAC,SAAS,EAAE,SAAS,CAAC,CAAC;IACjD,OAAO;QACL,OAAO,EAAE,GAAG;QACZ,GAAG;QACH,EAAE,EAAE,WAAW,CAAC,CAAC,CAAC,SAAS,CAAC,SAAS,EAAE,WAAW,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG;QAChE,eAAe,EAAE,MAAM;KACxB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,YAAY,CAAC,IAAkB,EAAE,KAAa,EAAE,MAAc;IAC5E,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,KAAK,GAAG,MAAM,CAAC,CAAC;IAC7C,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IAClC,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACpC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAChC,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;QAClC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QAChF,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;YAC/B,MAAM,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC;YAClC,MAAM,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;YAC/E,IAAI,GAAG,GAAG,CAAC,CAAC;YACZ,IAAI,KAAK,GAAG,CAAC,CAAC;YACd,KAAK,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;gBAChC,KAAK,IAAI,EAAE,GAAG,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC;oBAChC,GAAG,IAAI,IAAI,CAAC,MAAM,CAAC,EAAE,GAAG,IAAI,CAAC,KAAK,GAAG,EAAE,CAAC,CAAC;oBACzC,KAAK,EAAE,CAAC;gBACV,CAAC;YACH,CAAC;YACD,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACnD,CAAC;IACH,CAAC;IACD,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;AACxC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cbrowser",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.79.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Cognitive browser automation that thinks like your users—and helps AI agents navigate too. Simulate real user cognition with abandonment detection, constitutional safety, chaos engineering, and UX friction discovery. Sites that pass CBrowser's cognitive tests are easier for both humans and AI agents to navigate.",
|
|
6
6
|
"main": "dist/index.js",
|