cbrowser 18.77.0 → 18.78.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 +132 -0
- package/dist/visual/fixation-corpus.d.ts.map +1 -0
- package/dist/visual/fixation-corpus.js +212 -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,132 @@
|
|
|
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
|
+
export interface CorpusInfo {
|
|
50
|
+
name: string;
|
|
51
|
+
root: string;
|
|
52
|
+
stimuli: CorpusStimulus[];
|
|
53
|
+
horizons: FixationHorizon[];
|
|
54
|
+
/** Absent when the corpus ships only density maps and no raw logs. */
|
|
55
|
+
fixationLogDir?: string;
|
|
56
|
+
citation: string;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* One participant fixation as recorded by the tracker.
|
|
60
|
+
*
|
|
61
|
+
* Coordinates are NORMALIZED 0-1 in the source logs and stay that way here.
|
|
62
|
+
* Converting to pixels requires the stimulus dimensions, and doing it at load
|
|
63
|
+
* time — before the caller says which grid they are scoring against — is how
|
|
64
|
+
* coordinate-space bugs get baked in. `fixationsToGrid` does the conversion at
|
|
65
|
+
* the point the target size is actually known.
|
|
66
|
+
*/
|
|
67
|
+
export interface RawFixation {
|
|
68
|
+
/** 0-1, left to right. */
|
|
69
|
+
xNorm: number;
|
|
70
|
+
/** 0-1, top to bottom. */
|
|
71
|
+
yNorm: number;
|
|
72
|
+
/** Dwell in seconds. */
|
|
73
|
+
duration: number;
|
|
74
|
+
/** Seconds since stimulus onset — this is what makes horizon filtering possible. */
|
|
75
|
+
startTime: number;
|
|
76
|
+
participantId: string;
|
|
77
|
+
stimulusId: string;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Discover a UEyes-layout corpus.
|
|
81
|
+
*
|
|
82
|
+
* Returns null rather than throwing when the directory is absent: the corpus is
|
|
83
|
+
* 13GB and is not, and should not be, a dependency of the shipped package.
|
|
84
|
+
* Anything that needs it degrades to "not available" rather than failing.
|
|
85
|
+
*/
|
|
86
|
+
export declare function loadUEyesCorpus(root: string): CorpusInfo | null;
|
|
87
|
+
/**
|
|
88
|
+
* Parse one Gazepoint-format tracker log.
|
|
89
|
+
*
|
|
90
|
+
* The columns that matter: MEDIA_NAME (which stimulus), FPOGX/FPOGY (gaze point
|
|
91
|
+
* of regard, normalized), FPOGD (duration), FPOGS (start time), FPOGV (valid).
|
|
92
|
+
*
|
|
93
|
+
* INVALID SAMPLES ARE DROPPED, not clamped. FPOGV=0 means the tracker lost the
|
|
94
|
+
* eye — a blink, a look away, a calibration slip. Those rows still carry
|
|
95
|
+
* coordinates, and treating them as fixations manufactures ground truth from
|
|
96
|
+
* noise. Off-screen coordinates are dropped for the same reason.
|
|
97
|
+
*/
|
|
98
|
+
export declare function parseGazepointLog(logPath: string): RawFixation[];
|
|
99
|
+
/**
|
|
100
|
+
* Keep only fixations that occurred within `seconds` of stimulus onset.
|
|
101
|
+
*
|
|
102
|
+
* This is what lets a persona be scored against the horizon that matches it: an
|
|
103
|
+
* impatient persona compared to 1s of human viewing, a thorough one to 7s.
|
|
104
|
+
* Comparing every persona to the same aggregate map discards the one dimension
|
|
105
|
+
* this corpus was built to expose.
|
|
106
|
+
*/
|
|
107
|
+
export declare function filterByHorizon(fixations: RawFixation[], seconds: number): RawFixation[];
|
|
108
|
+
/** Convert normalized fixations to a grid's pixel space. */
|
|
109
|
+
export declare function toGridFixations(fixations: RawFixation[], gridWidth: number, gridHeight: number): Fixation[];
|
|
110
|
+
/**
|
|
111
|
+
* Build a fixation density map by accumulating Gaussian blobs.
|
|
112
|
+
*
|
|
113
|
+
* Ground-truth maps ship with UEyes, but a corpus that provides only raw logs
|
|
114
|
+
* needs this to score CC at all. Sigma is in grid cells; the literature
|
|
115
|
+
* convention is roughly one degree of visual angle, and for a 32x32 analysis
|
|
116
|
+
* grid that lands near 1.
|
|
117
|
+
*/
|
|
118
|
+
export declare function fixationsToGrid(fixations: Fixation[], width: number, height: number, sigma?: number): SaliencyGrid;
|
|
119
|
+
/**
|
|
120
|
+
* Deterministic train/test split.
|
|
121
|
+
*
|
|
122
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
123
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
124
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
125
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
126
|
+
* runs and machines without storing a manifest.
|
|
127
|
+
*/
|
|
128
|
+
export declare function splitCorpus(corpus: CorpusInfo, testFraction?: number): {
|
|
129
|
+
train: CorpusStimulus[];
|
|
130
|
+
test: CorpusStimulus[];
|
|
131
|
+
};
|
|
132
|
+
//# 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;CACxD;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;CAClB;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;;;;;;;;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,212 @@
|
|
|
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
|
+
* Deterministic train/test split.
|
|
190
|
+
*
|
|
191
|
+
* Calibrating on a corpus and then reporting accuracy on the same corpus is
|
|
192
|
+
* measuring how well the parameters were fitted, not how well the model
|
|
193
|
+
* generalises — and it is the single easiest way to publish a number that does
|
|
194
|
+
* not survive review. The split is by stimulus id hash so it is stable across
|
|
195
|
+
* runs and machines without storing a manifest.
|
|
196
|
+
*/
|
|
197
|
+
export function splitCorpus(corpus, testFraction = 0.3) {
|
|
198
|
+
const hash = (s) => {
|
|
199
|
+
let h = 2166136261;
|
|
200
|
+
for (let i = 0; i < s.length; i++) {
|
|
201
|
+
h ^= s.charCodeAt(i);
|
|
202
|
+
h = Math.imul(h, 16777619);
|
|
203
|
+
}
|
|
204
|
+
return (h >>> 0) / 4294967296;
|
|
205
|
+
};
|
|
206
|
+
const train = [];
|
|
207
|
+
const test = [];
|
|
208
|
+
for (const s of corpus.stimuli)
|
|
209
|
+
(hash(s.id) < testFraction ? test : train).push(s);
|
|
210
|
+
return { train, test };
|
|
211
|
+
}
|
|
212
|
+
//# 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":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cbrowser",
|
|
3
|
-
"version": "18.
|
|
3
|
+
"version": "18.78.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",
|