@photivra/engine 2.7.0 → 2.8.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/CHANGELOG.md +86 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/native-environment-lean.d.ts +35 -5
- package/dist/capture/native-environment-lean.js +175 -16
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-raw.d.ts +9 -1
- package/dist/capture/native-environment-raw.js +14 -9
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +58 -0
- package/dist/capture/native-environment-tabulated.js +91 -0
- package/dist/capture/native-environment-tabulated.js.map +1 -0
- package/dist/core/internal-radiance-evaluator.d.ts +28 -0
- package/dist/core/internal-radiance-evaluator.js +30 -0
- package/dist/core/internal-radiance-evaluator.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/diffraction-provider.js +30 -2
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +14 -83
- package/dist/facade/display-view.js +331 -846
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +65 -0
- package/dist/facade/expected-signal-setup.js +56 -0
- package/dist/facade/expected-signal-setup.js.map +1 -0
- package/dist/facade/expected-signal.d.ts +83 -0
- package/dist/facade/expected-signal.js +571 -0
- package/dist/facade/expected-signal.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +6 -0
- package/dist/facade/scene-radiance.js +31 -10
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.js +65 -16
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +26 -49
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/docs/API_REFERENCE.md +1 -1
- package/docs/QUICK_START.md +1 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +2 -2
- package/package.json +1 -1
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
* added as independent per-pixel variance, weighted by Σ_j (w_j/A)², as is the output quantization (Δ²/12).
|
|
33
33
|
* One correlated normal draw per display pixel (the sum of hundreds of independent site noises) gives the noise.
|
|
34
34
|
*/
|
|
35
|
-
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
36
35
|
import { createDeterministicUniformStream, createPortableStandardNormalPairs, deriveNativeSiteStreamSeedUint32, fmix32, samplePortableStandardNormal } from "../sensor/stochastic-seed.js";
|
|
37
36
|
import { encodeSrgbComponentPortable } from "../color/srgb-transfer.js";
|
|
38
37
|
import { createPoissonSampler } from "../sensor/raw-readout.js";
|
|
@@ -41,569 +40,7 @@ import { portableCbrt, portableCosTwoPi, portableExp, portableExp2, portableLog2
|
|
|
41
40
|
export const DISPLAY_VIEW_JITTER_STREAM_ID = 32;
|
|
42
41
|
export const DISPLAY_VIEW_NOISE_STREAM_ID = 33;
|
|
43
42
|
export const DISPLAY_VIEW_AHD_STREAM_ID = 34;
|
|
44
|
-
|
|
45
|
-
/** Largest cache window, in sites or lattice points; larger windows are evaluated without caching. */
|
|
46
|
-
const CACHE_LIMIT = 1 << 22;
|
|
47
|
-
/** Largest band of looked-up lattice materials per group, in lattice points (a band that would be larger restarts at the window). */
|
|
48
|
-
const BAND_LIMIT = 1 << 22;
|
|
49
|
-
/**
|
|
50
|
-
* Expected photo electrons per site: μ_c(i, j) = Σ_o w_o Σ_λ A_c(λ) Σ_t K_λ(t)·L(p_o(i, j) − t, λ), where p_o is the
|
|
51
|
-
* query point of offset o and K the diffraction kernel (a single unit tap without diffraction). This is the sum the
|
|
52
|
-
* facade's environment chain forms for a still, rectilinear, distortion-free planar scene, where the
|
|
53
|
-
* radiance-to-irradiance factor is the same at every site (docs/DISPLAY_VIEW.md, "Expected signal").
|
|
54
|
-
*
|
|
55
|
-
* With diffraction, offsets that share a fractional shift read one lattice, and their kernels are combined in advance:
|
|
56
|
-
* C_c(d) = Σ_members w·Σ_λ A_c(λ)·K_λ(t)·L(λ) over the taps t that reach lattice offset d. A neutral material's radiance
|
|
57
|
-
* is its reflectance times the white radiance, so all neutral materials share one combined kernel per channel.
|
|
58
|
-
*/
|
|
59
|
-
export function createDisplayViewExpectedSignal(input) {
|
|
60
|
-
const W = input.nativeWidth, H = input.nativeHeight, g = 2 / input.outputWidth, nodes = input.wavelengthsNm.length;
|
|
61
|
-
const R = input.kernels ? input.kernels.radius : 0, size = 2 * R + 1;
|
|
62
|
-
if (input.kernels && input.kernels.perNode.length !== nodes)
|
|
63
|
-
throw new InvalidConfigurationError("Display view kernels must match the spectral nodes.");
|
|
64
|
-
if (!input.kernels && !input.queryPoint)
|
|
65
|
-
throw new InvalidConfigurationError("Display view needs the environment query points without diffraction.");
|
|
66
|
-
const A = input.electronsPerRadiance, scene = input.scene, offsets = input.offsets, channelAt = input.channelAt;
|
|
67
|
-
const kernelAt = (node, t) => (input.kernels ? input.kernels.perNode[node][t] : 1);
|
|
68
|
-
// Materials: neutral ones by reflectance, spectral ones by index.
|
|
69
|
-
let reflectance = new Float64Array(0), spectralIndex = new Int32Array(0);
|
|
70
|
-
const spectral = [];
|
|
71
|
-
if (scene.kind === "materials") {
|
|
72
|
-
reflectance = new Float64Array(scene.materials.length);
|
|
73
|
-
spectralIndex = new Int32Array(scene.materials.length).fill(-1);
|
|
74
|
-
scene.materials.forEach((m, index) => { if ("neutral" in m)
|
|
75
|
-
reflectance[index] = m.neutral;
|
|
76
|
-
else {
|
|
77
|
-
spectralIndex[index] = spectral.length;
|
|
78
|
-
spectral.push(m.radiance);
|
|
79
|
-
} });
|
|
80
|
-
}
|
|
81
|
-
// Offsets grouped by fractional shift: offsets that differ by whole sites share one lattice.
|
|
82
|
-
const frac = (v) => { const f = v - Math.floor(v); return f > 1 - OFFSET_TOLERANCE || f < OFFSET_TOLERANCE ? 0 : f; };
|
|
83
|
-
const groups = [];
|
|
84
|
-
for (const o of offsets) {
|
|
85
|
-
const fx = frac(o.x), fy = frac(o.y), ix = Math.round(o.x - fx), iy = Math.round(o.y - fy);
|
|
86
|
-
let group = groups.find((q) => Math.abs(q.fx - fx) <= OFFSET_TOLERANCE && Math.abs(q.fy - fy) <= OFFSET_TOLERANCE);
|
|
87
|
-
if (!group) {
|
|
88
|
-
group = { fx, fy, members: [], minIx: ix, maxIx: ix, minIy: iy, maxIy: iy, cw: 0, ch: 0, neutral: [], spectral: [], perNode: [], cache: new Int32Array(0), cx0: 0, cy0: 0, cpw: 0, cph: 0 };
|
|
89
|
-
groups.push(group);
|
|
90
|
-
}
|
|
91
|
-
group.members.push({ ix, iy, weight: o.weight });
|
|
92
|
-
group.minIx = Math.min(group.minIx, ix);
|
|
93
|
-
group.maxIx = Math.max(group.maxIx, ix);
|
|
94
|
-
group.minIy = Math.min(group.minIy, iy);
|
|
95
|
-
group.maxIy = Math.max(group.maxIy, iy);
|
|
96
|
-
}
|
|
97
|
-
if (input.kernels)
|
|
98
|
-
for (const group of groups) {
|
|
99
|
-
// Combined kernel over lattice offsets d = (ix − tu, iy − tv), stored at (d − min + R).
|
|
100
|
-
const cw = group.maxIx - group.minIx + size, ch = group.maxIy - group.minIy + size;
|
|
101
|
-
group.cw = cw;
|
|
102
|
-
group.ch = ch;
|
|
103
|
-
const combine = (value) => [0, 1, 2].map((c) => {
|
|
104
|
-
const out = new Float64Array(cw * ch);
|
|
105
|
-
for (const member of group.members)
|
|
106
|
-
for (let tv = -R, t = 0; tv <= R; tv++)
|
|
107
|
-
for (let tu = -R; tu <= R; tu++, t++) {
|
|
108
|
-
let sum = 0;
|
|
109
|
-
for (let n = 0; n < nodes; n++)
|
|
110
|
-
sum += value(c, n, t);
|
|
111
|
-
const index = (member.iy - tv - group.minIy + R) * cw + (member.ix - tu - group.minIx + R);
|
|
112
|
-
out[index] = out[index] + member.weight * sum;
|
|
113
|
-
}
|
|
114
|
-
return out;
|
|
115
|
-
});
|
|
116
|
-
if (scene.kind === "materials") {
|
|
117
|
-
group.neutral = combine((c, n, t) => A[c][n] * kernelAt(n, t) * scene.whiteRadiance[n]);
|
|
118
|
-
const perMaterial = spectral.map((radiance) => combine((c, n, t) => A[c][n] * kernelAt(n, t) * radiance[n]));
|
|
119
|
-
group.spectral = [0, 1, 2].map((c) => perMaterial.map((tables) => tables[c]));
|
|
120
|
-
}
|
|
121
|
-
else {
|
|
122
|
-
// Radiance scenes: combined kernels per node, applied to the cached radiance at each node.
|
|
123
|
-
group.perNode = Array.from({ length: nodes }, (_, n) => combine((c, node, t) => (node === n ? A[c][n] * kernelAt(n, t) : 0)));
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
// Neutral tap-0 signal per channel and spectral per material, for the direct (no-kernel) path.
|
|
127
|
-
const neutralDirect = scene.kind === "materials" ? [0, 1, 2].map((c) => { let s = 0; for (let n = 0; n < nodes; n++)
|
|
128
|
-
s += A[c][n] * scene.whiteRadiance[n]; return s; }) : [];
|
|
129
|
-
const spectralDirect = [0, 1, 2].map((c) => spectral.map((radiance) => { let s = 0; for (let n = 0; n < nodes; n++)
|
|
130
|
-
s += A[c][n] * radiance[n]; return s; }));
|
|
131
|
-
let lookups = 0;
|
|
132
|
-
const point = new Float64Array(2);
|
|
133
|
-
const materialSignalDirect = (m, c) => {
|
|
134
|
-
const si = spectralIndex[m];
|
|
135
|
-
if (si === undefined)
|
|
136
|
-
throw new InvalidConfigurationError("Unknown chart material.");
|
|
137
|
-
return si < 0 ? reflectance[m] * neutralDirect[c] : spectralDirect[c][si];
|
|
138
|
-
};
|
|
139
|
-
/** Without diffraction: one site from the environment's own query points. */
|
|
140
|
-
function directSite(x, y) {
|
|
141
|
-
const c = channelAt(x, y);
|
|
142
|
-
let sum = 0;
|
|
143
|
-
for (let o = 0; o < offsets.length; o++) {
|
|
144
|
-
input.queryPoint(x, y, o, point);
|
|
145
|
-
let value;
|
|
146
|
-
if (scene.kind === "materials") {
|
|
147
|
-
value = materialSignalDirect(scene.materialAt(point[0], point[1]), c);
|
|
148
|
-
lookups++;
|
|
149
|
-
}
|
|
150
|
-
else {
|
|
151
|
-
value = 0;
|
|
152
|
-
for (let n = 0; n < nodes; n++)
|
|
153
|
-
value += A[c][n] * scene.radianceAt(point[0], point[1], input.wavelengthsNm[n]);
|
|
154
|
-
lookups += nodes;
|
|
155
|
-
}
|
|
156
|
-
sum += offsets[o].weight * value;
|
|
157
|
-
}
|
|
158
|
-
return sum;
|
|
159
|
-
}
|
|
160
|
-
// Lattice point (p, q) of a group: the facade's lattice formula (createDiffractedPlanarProvider).
|
|
161
|
-
const latticeU = (group, p) => (group.fx === 0 ? (p + 0.5 - W / 2) * g : (p + 0.5 + group.fx - W / 2) * g);
|
|
162
|
-
const latticeV = (group, q) => (group.fy === 0 ? -(q + 0.5 - H / 2) * g : -(q + 0.5 + group.fy - H / 2) * g);
|
|
163
|
-
const radianceScratch = new Float64Array(nodes);
|
|
164
|
-
/** Material (or radiance at each node, into radianceScratch) of a group's lattice point, from the window cache when it holds it. */
|
|
165
|
-
function latticeMaterial(group, p, q) {
|
|
166
|
-
const i = p - group.cx0, j = q - group.cy0, cached = i >= 0 && j >= 0 && i < group.cpw && j < group.cph;
|
|
167
|
-
if (cached) {
|
|
168
|
-
const m = group.cache[j * group.cpw + i];
|
|
169
|
-
if (m >= 0)
|
|
170
|
-
return m;
|
|
171
|
-
}
|
|
172
|
-
const m = scene.materialAt(latticeU(group, p), latticeV(group, q));
|
|
173
|
-
lookups++;
|
|
174
|
-
if (cached)
|
|
175
|
-
group.cache[j * group.cpw + i] = m;
|
|
176
|
-
return m;
|
|
177
|
-
}
|
|
178
|
-
function latticeRadiance(group, p, q) {
|
|
179
|
-
const i = p - group.cx0, j = q - group.cy0, cached = i >= 0 && j >= 0 && i < group.cpw && j < group.cph, cache = group.cache;
|
|
180
|
-
if (cached && !Number.isNaN(cache[(j * group.cpw + i) * nodes])) {
|
|
181
|
-
for (let n = 0; n < nodes; n++)
|
|
182
|
-
radianceScratch[n] = cache[(j * group.cpw + i) * nodes + n];
|
|
183
|
-
return radianceScratch;
|
|
184
|
-
}
|
|
185
|
-
const u = latticeU(group, p), v = latticeV(group, q);
|
|
186
|
-
for (let n = 0; n < nodes; n++)
|
|
187
|
-
radianceScratch[n] = scene.radianceAt(u, v, input.wavelengthsNm[n]);
|
|
188
|
-
lookups += nodes;
|
|
189
|
-
if (cached)
|
|
190
|
-
for (let n = 0; n < nodes; n++)
|
|
191
|
-
cache[(j * group.cpw + i) * nodes + n] = radianceScratch[n];
|
|
192
|
-
return radianceScratch;
|
|
193
|
-
}
|
|
194
|
-
/** With diffraction: one site through the combined kernels on each group's lattice. */
|
|
195
|
-
function kernelSite(x, y) {
|
|
196
|
-
const c = channelAt(x, y);
|
|
197
|
-
let sum = 0;
|
|
198
|
-
for (const group of groups) {
|
|
199
|
-
const p0 = x + group.minIx - R, q0 = y + group.minIy - R, cw = group.cw, ch = group.ch;
|
|
200
|
-
if (scene.kind === "materials") {
|
|
201
|
-
const nk = group.neutral[c], sk = group.spectral[c];
|
|
202
|
-
for (let dy = 0; dy < ch; dy++)
|
|
203
|
-
for (let dx = 0; dx < cw; dx++) {
|
|
204
|
-
const m = latticeMaterial(group, p0 + dx, q0 + dy), si = spectralIndex[m];
|
|
205
|
-
if (si === undefined)
|
|
206
|
-
throw new InvalidConfigurationError("Unknown chart material.");
|
|
207
|
-
sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
else {
|
|
211
|
-
for (let dy = 0; dy < ch; dy++)
|
|
212
|
-
for (let dx = 0; dx < cw; dx++) {
|
|
213
|
-
const radiance = latticeRadiance(group, p0 + dx, q0 + dy);
|
|
214
|
-
for (let n = 0; n < nodes; n++)
|
|
215
|
-
sum += group.perNode[n][c][dy * cw + dx] * radiance[n];
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return sum;
|
|
220
|
-
}
|
|
221
|
-
// Filled windows (chart materials with diffraction, every lattice point of the window cacheable): each group keeps a band of
|
|
222
|
-
// looked-up lattice materials that covers the window's rows and grows to the right as the windows of a display row move
|
|
223
|
-
// right, with summed-area tables of material changes. A site whose kernel reads one material only (each group's rectangle
|
|
224
|
-
// uniform, the same material in all) takes the value of the same sum over that material, precomputed below in the same
|
|
225
|
-
// order; other sites run the same sum from the band. Either way each site's value is the same sequence of operations as
|
|
226
|
-
// kernelSite, so it is bit-identical. The scene lookup count stays the count kernelSite's lazy window cache makes (each
|
|
227
|
-
// cache point a computed site reads counts once per window, points outside the cache once per read): the rectangles of
|
|
228
|
-
// the sites computed in a window are counted when the window closes (windowLookups).
|
|
229
|
-
const uniformSite = scene.kind === "materials" && input.kernels ? [0, 1, 2].map((c) => Float64Array.from(scene.materials, (_, m) => {
|
|
230
|
-
const si = spectralIndex[m];
|
|
231
|
-
let sum = 0;
|
|
232
|
-
for (const group of groups) {
|
|
233
|
-
const nk = group.neutral[c], sk = si < 0 ? nk : group.spectral[c][si];
|
|
234
|
-
for (let t = 0; t < group.cw * group.ch; t++)
|
|
235
|
-
sum += si < 0 ? reflectance[m] * nk[t] : sk[t];
|
|
236
|
-
}
|
|
237
|
-
return sum;
|
|
238
|
-
})) : [];
|
|
239
|
-
const allNeutral = spectral.length === 0;
|
|
240
|
-
const bands = groups.map(() => ({ x0: 0, y0: 0, rows: 0, capacity: 0, filled: 0, material: new Int32Array(0), reflectance: new Float64Array(0),
|
|
241
|
-
changeLeft: new Int32Array(0), changeUp: new Int32Array(0), v: new Float64Array(0) }));
|
|
242
|
-
/** Makes the band of group g cover lattice columns [x0, x1) of rows [y0, y0 + rows), extending it to the right when it can. */
|
|
243
|
-
function coverBand(g, x0, x1, y0, rows) {
|
|
244
|
-
const group = groups[g], band = bands[g], materials = scene;
|
|
245
|
-
if (!(band.rows === rows && band.y0 === y0 && x0 >= band.x0 && x1 - band.x0 <= band.capacity)) {
|
|
246
|
-
const capacity = Math.max(x1 - x0, Math.min(Math.floor(BAND_LIMIT / rows), W + 2 * (x1 - x0)));
|
|
247
|
-
const size = rows * capacity, tables = (rows + 1) * (capacity + 1);
|
|
248
|
-
band.x0 = x0;
|
|
249
|
-
band.y0 = y0;
|
|
250
|
-
band.rows = rows;
|
|
251
|
-
band.capacity = capacity;
|
|
252
|
-
band.filled = 0;
|
|
253
|
-
if (band.material.length < size) {
|
|
254
|
-
band.material = new Int32Array(size);
|
|
255
|
-
band.reflectance = new Float64Array(allNeutral ? size : 0);
|
|
256
|
-
}
|
|
257
|
-
if (band.changeLeft.length < tables) {
|
|
258
|
-
band.changeLeft = new Int32Array(tables);
|
|
259
|
-
band.changeUp = new Int32Array(tables);
|
|
260
|
-
}
|
|
261
|
-
band.changeLeft.fill(0, 0, capacity + 1);
|
|
262
|
-
band.changeUp.fill(0, 0, capacity + 1);
|
|
263
|
-
for (let j = 0; j < rows; j++) {
|
|
264
|
-
band.changeLeft[(j + 1) * (capacity + 1)] = 0;
|
|
265
|
-
band.changeUp[(j + 1) * (capacity + 1)] = 0;
|
|
266
|
-
}
|
|
267
|
-
if (band.v.length < rows)
|
|
268
|
-
band.v = new Float64Array(rows);
|
|
269
|
-
for (let j = 0; j < rows; j++)
|
|
270
|
-
band.v[j] = latticeV(group, y0 + j);
|
|
271
|
-
}
|
|
272
|
-
const stride = band.capacity, S1 = stride + 1, material = band.material, left = band.changeLeft, up = band.changeUp;
|
|
273
|
-
for (let i = band.filled; i < x1 - band.x0; i++) {
|
|
274
|
-
const u = latticeU(group, band.x0 + i);
|
|
275
|
-
for (let j = 0; j < rows; j++) {
|
|
276
|
-
const m = materials.materialAt(u, band.v[j]);
|
|
277
|
-
if (spectralIndex[m] === undefined)
|
|
278
|
-
throw new InvalidConfigurationError("Unknown chart material.");
|
|
279
|
-
material[j * stride + i] = m;
|
|
280
|
-
if (allNeutral)
|
|
281
|
-
band.reflectance[j * stride + i] = reflectance[m];
|
|
282
|
-
const changedLeft = i > 0 && material[j * stride + i - 1] !== m ? 1 : 0, changedUp = j > 0 && material[(j - 1) * stride + i] !== m ? 1 : 0;
|
|
283
|
-
left[(j + 1) * S1 + i + 1] = left[j * S1 + i + 1] + left[(j + 1) * S1 + i] - left[j * S1 + i] + changedLeft;
|
|
284
|
-
up[(j + 1) * S1 + i + 1] = up[j * S1 + i + 1] + up[(j + 1) * S1 + i] - up[j * S1 + i] + changedUp;
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
band.filled = Math.max(band.filled, x1 - band.x0);
|
|
288
|
-
}
|
|
289
|
-
const rectSum = (sat, S1, i0, j0, i1, j1) => sat[j1 * S1 + i1] - sat[j0 * S1 + i1] - sat[j1 * S1 + i0] + sat[j0 * S1 + i0];
|
|
290
|
-
let filled = false;
|
|
291
|
-
/** Sites outside the site window computed in this window (x, y pairs in window coordinates); their rectangles count when the window closes. */
|
|
292
|
-
let outsideSites = [];
|
|
293
|
-
let marks = new Uint8Array(0), cover = new Uint8Array(0);
|
|
294
|
-
/** A site of a filled window: (i, j) are its coordinates in the site window, inside or outside it. */
|
|
295
|
-
function filledSite(x, y, i, j) {
|
|
296
|
-
const c = channelAt(x, y), inside = i >= 0 && j >= 0 && i < sw && j < sh;
|
|
297
|
-
if (inside) {
|
|
298
|
-
let m = -1;
|
|
299
|
-
for (let g = 0; g < groups.length; g++) {
|
|
300
|
-
const group = groups[g], band = bands[g], S1 = band.capacity + 1, bi = x + group.minIx - R - band.x0, bj = y + group.minIy - R - band.y0;
|
|
301
|
-
if (rectSum(band.changeLeft, S1, bi + 1, bj, bi + group.cw, bj + group.ch) !== 0 || rectSum(band.changeUp, S1, bi, bj + 1, bi + group.cw, bj + group.ch) !== 0) {
|
|
302
|
-
m = -2;
|
|
303
|
-
break;
|
|
304
|
-
}
|
|
305
|
-
const own = band.material[bj * band.capacity + bi];
|
|
306
|
-
if (m === -1)
|
|
307
|
-
m = own;
|
|
308
|
-
else if (own !== m) {
|
|
309
|
-
m = -2;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
if (m >= 0) {
|
|
314
|
-
lastMaterial = m;
|
|
315
|
-
return uniformSite[c][m];
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
lastMaterial = -1;
|
|
319
|
-
let sum = 0;
|
|
320
|
-
for (let g = 0; g < groups.length; g++) {
|
|
321
|
-
const group = groups[g], band = bands[g], cw = group.cw, ch = group.ch, nk = group.neutral[c], sk = group.spectral[c], stride = band.capacity;
|
|
322
|
-
const p0 = x + group.minIx - R, q0 = y + group.minIy - R, bi = p0 - band.x0, bj = q0 - band.y0;
|
|
323
|
-
if (inside && allNeutral) {
|
|
324
|
-
const refl = band.reflectance;
|
|
325
|
-
for (let dy = 0, t = 0; dy < ch; dy++) {
|
|
326
|
-
const row = (bj + dy) * stride + bi;
|
|
327
|
-
for (let dx = 0; dx < cw; dx++, t++)
|
|
328
|
-
sum += refl[row + dx] * nk[t];
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
else if (inside) {
|
|
332
|
-
const material = band.material;
|
|
333
|
-
for (let dy = 0, t = 0; dy < ch; dy++) {
|
|
334
|
-
const row = (bj + dy) * stride + bi;
|
|
335
|
-
for (let dx = 0; dx < cw; dx++, t++) {
|
|
336
|
-
const m = material[row + dx], si = spectralIndex[m];
|
|
337
|
-
sum += si < 0 ? reflectance[m] * nk[t] : sk[si][t];
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
else {
|
|
342
|
-
for (let dy = 0; dy < ch; dy++)
|
|
343
|
-
for (let dx = 0; dx < cw; dx++) {
|
|
344
|
-
const ci = i + dx, cj = j + dy;
|
|
345
|
-
let m;
|
|
346
|
-
if (ci >= 0 && cj >= 0 && ci < group.cpw && cj < group.cph)
|
|
347
|
-
m = band.material[(bj + dy) * stride + bi + dx];
|
|
348
|
-
else
|
|
349
|
-
m = scene.materialAt(latticeU(group, p0 + dx), latticeV(group, q0 + dy));
|
|
350
|
-
const si = spectralIndex[m];
|
|
351
|
-
if (si === undefined)
|
|
352
|
-
throw new InvalidConfigurationError("Unknown chart material.");
|
|
353
|
-
sum += si < 0 ? reflectance[m] * nk[dy * cw + dx] : sk[si][dy * cw + dx];
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
return sum;
|
|
358
|
-
}
|
|
359
|
-
/** Lattice points of an outside site's rectangles that lie outside the window caches (kernelSite looks these up on every read). */
|
|
360
|
-
function outsideLookups(i, j) {
|
|
361
|
-
let count = 0;
|
|
362
|
-
for (const group of groups) {
|
|
363
|
-
const w = Math.max(0, Math.min(i + group.cw, group.cpw) - Math.max(i, 0)), h = Math.max(0, Math.min(j + group.ch, group.cph) - Math.max(j, 0));
|
|
364
|
-
count += group.cw * group.ch - w * h;
|
|
365
|
-
}
|
|
366
|
-
return count;
|
|
367
|
-
}
|
|
368
|
-
// Site values of filled windows persist across windows (a site's value does not depend on the window): rows of native
|
|
369
|
-
// sites in a ring of a power-of-two number of rows, row y in slot y mod ringRows. The window's site cache still holds the
|
|
370
|
-
// sites read in the window, which is also what its lookup count needs.
|
|
371
|
-
let ringRows = 0, ringOn = false, ring = new Float64Array(0), ringMaterial = new Int32Array(0), ringRowId = new Int32Array(0), windowMaterial = new Int32Array(0), lastMaterial = -1;
|
|
372
|
-
/** A filled-window site read outside the site window: counted as kernelSite counts it. */
|
|
373
|
-
function outsideRead(i, j) { lookups += outsideLookups(i, j); outsideSites.push(i, j); }
|
|
374
|
-
/** A site from the ring; on a miss (re)claims the row's slot when another row holds it, computes the site and stores it. */
|
|
375
|
-
function ringRead(x, y, i, j) {
|
|
376
|
-
const slot = y & (ringRows - 1), q = slot * W + x;
|
|
377
|
-
if (ringRowId[slot] === y) {
|
|
378
|
-
const v = ring[q];
|
|
379
|
-
if (v === v) {
|
|
380
|
-
lastMaterial = ringMaterial[q];
|
|
381
|
-
return v;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
ringRowId[slot] = y;
|
|
386
|
-
ring.fill(NaN, slot * W, (slot + 1) * W);
|
|
387
|
-
}
|
|
388
|
-
const v = filledSite(x, y, i, j);
|
|
389
|
-
ring[q] = v;
|
|
390
|
-
ringMaterial[q] = lastMaterial;
|
|
391
|
-
return v;
|
|
392
|
-
}
|
|
393
|
-
// Without diffraction (#392): site values persist across windows the same way, in their own ring (a site's value does not
|
|
394
|
-
// depend on the window). A ring hit counts the lookups directSite would have made, so `lookups` is unchanged.
|
|
395
|
-
let directRows = 0, directOn = false, directRing = new Float64Array(0), directRowId = new Int32Array(0);
|
|
396
|
-
const directLookups = offsets.length * (scene.kind === "materials" ? 1 : nodes);
|
|
397
|
-
function directRead(x, y) {
|
|
398
|
-
const slot = y & (directRows - 1), q = slot * W + x;
|
|
399
|
-
if (directRowId[slot] === y) {
|
|
400
|
-
const v = directRing[q];
|
|
401
|
-
if (v === v) {
|
|
402
|
-
lookups += directLookups;
|
|
403
|
-
return v;
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
else {
|
|
407
|
-
directRowId[slot] = y;
|
|
408
|
-
directRing.fill(NaN, slot * W, (slot + 1) * W);
|
|
409
|
-
}
|
|
410
|
-
const v = directSite(x, y);
|
|
411
|
-
directRing[q] = v;
|
|
412
|
-
return v;
|
|
413
|
-
}
|
|
414
|
-
/** A site not in the window's site cache: from a ring, a filled window without the ring, the lazy window caches, or uncached. */
|
|
415
|
-
function siteMiss(x, y, i, j, cached) {
|
|
416
|
-
if (directOn) {
|
|
417
|
-
const v = directRead(x, y);
|
|
418
|
-
if (cached)
|
|
419
|
-
siteCache[j * sw + i] = v;
|
|
420
|
-
return v;
|
|
421
|
-
}
|
|
422
|
-
if (ringOn) {
|
|
423
|
-
if (!cached) {
|
|
424
|
-
outsideRead(i, j);
|
|
425
|
-
return ringRead(x, y, i, j);
|
|
426
|
-
}
|
|
427
|
-
const v = ringRead(x, y, i, j);
|
|
428
|
-
siteCache[j * sw + i] = v;
|
|
429
|
-
windowMaterial[j * sw + i] = lastMaterial;
|
|
430
|
-
return v;
|
|
431
|
-
}
|
|
432
|
-
if (filled && !cached)
|
|
433
|
-
outsideRead(i, j);
|
|
434
|
-
const v = filled ? filledSite(x, y, i, j) : input.kernels ? kernelSite(x, y) : directSite(x, y);
|
|
435
|
-
if (cached)
|
|
436
|
-
siteCache[j * sw + i] = v;
|
|
437
|
-
return v;
|
|
438
|
-
}
|
|
439
|
-
/**
|
|
440
|
-
* Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
|
|
441
|
-
* covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
|
|
442
|
-
*/
|
|
443
|
-
function windowLookups() {
|
|
444
|
-
let total = 0;
|
|
445
|
-
for (const group of groups) {
|
|
446
|
-
const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1;
|
|
447
|
-
// Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
|
|
448
|
-
if (marks.length < ew * eh) {
|
|
449
|
-
marks = new Uint8Array(ew * eh);
|
|
450
|
-
}
|
|
451
|
-
let x0 = ew, x1 = -1, y0 = eh, y1 = -1;
|
|
452
|
-
const mark = (ei, ej) => {
|
|
453
|
-
if (ei < 0 || ej < 0 || ei >= ew || ej >= eh)
|
|
454
|
-
return;
|
|
455
|
-
marks[ej * ew + ei] = 1;
|
|
456
|
-
if (ei < x0)
|
|
457
|
-
x0 = ei;
|
|
458
|
-
if (ei > x1)
|
|
459
|
-
x1 = ei;
|
|
460
|
-
if (ej < y0)
|
|
461
|
-
y0 = ej;
|
|
462
|
-
if (ej > y1)
|
|
463
|
-
y1 = ej;
|
|
464
|
-
};
|
|
465
|
-
for (let j = 0; j < sh; j++)
|
|
466
|
-
for (let i = 0; i < sw; i++)
|
|
467
|
-
if (!Number.isNaN(siteCache[j * sw + i]))
|
|
468
|
-
mark(i + cw - 1, j + ch - 1);
|
|
469
|
-
for (let k = 0; k < outsideSites.length; k += 2)
|
|
470
|
-
mark(outsideSites[k] + cw - 1, outsideSites[k + 1] + ch - 1);
|
|
471
|
-
if (x1 < 0)
|
|
472
|
-
continue;
|
|
473
|
-
// Cache point (i, j) is covered when a mark lies in extended columns [i, i + cw) and rows [j, j + ch): a horizontal
|
|
474
|
-
// then a vertical running count over the marks' box (outside it nothing is covered).
|
|
475
|
-
const i0 = Math.max(0, x0 - cw + 1), i1 = Math.min(cpw - 1, x1), cwid = i1 - i0 + 1;
|
|
476
|
-
if (cover.length < cwid * (y1 - y0 + 1))
|
|
477
|
-
cover = new Uint8Array(cwid * (y1 - y0 + 1));
|
|
478
|
-
for (let r = y0; r <= y1; r++) {
|
|
479
|
-
let run = 0;
|
|
480
|
-
for (let e = i0; e <= i1 + cw - 1; e++) {
|
|
481
|
-
run += marks[r * ew + e];
|
|
482
|
-
if (e - cw >= i0)
|
|
483
|
-
run -= marks[r * ew + e - cw];
|
|
484
|
-
if (e >= i0 + cw - 1)
|
|
485
|
-
cover[(r - y0) * cwid + e - cw + 1 - i0] = run > 0 ? 1 : 0;
|
|
486
|
-
}
|
|
487
|
-
marks.fill(0, r * ew + x0, r * ew + x1 + 1);
|
|
488
|
-
}
|
|
489
|
-
for (let i = 0; i < cwid; i++) {
|
|
490
|
-
let run = 0;
|
|
491
|
-
for (let r = y0; r <= y1 + ch - 1; r++) {
|
|
492
|
-
if (r <= y1)
|
|
493
|
-
run += cover[(r - y0) * cwid + i];
|
|
494
|
-
if (r - ch >= y0 && r - ch <= y1)
|
|
495
|
-
run -= cover[(r - ch - y0) * cwid + i];
|
|
496
|
-
const j = r - ch + 1;
|
|
497
|
-
if (j >= 0 && j < cph && run > 0)
|
|
498
|
-
total++;
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
}
|
|
502
|
-
return total;
|
|
503
|
-
}
|
|
504
|
-
let sx0 = 0, sy0 = 0, sw = 0, sh = 0, siteCache = new Float64Array(0);
|
|
505
|
-
const pilotNode = input.wavelengthsNm[Math.floor(nodes / 2)];
|
|
506
|
-
return {
|
|
507
|
-
get lookups() { return lookups + (filled ? windowLookups() : 0); },
|
|
508
|
-
get uniformMaterial() { return lastMaterial; },
|
|
509
|
-
get tracksMaterial() { return ringOn; },
|
|
510
|
-
get cachesSites() { return sw > 0; },
|
|
511
|
-
window(x0, y0, x1, y1) {
|
|
512
|
-
if (filled)
|
|
513
|
-
lookups += windowLookups();
|
|
514
|
-
filled = false;
|
|
515
|
-
ringOn = false;
|
|
516
|
-
outsideSites = [];
|
|
517
|
-
sx0 = x0;
|
|
518
|
-
sy0 = y0;
|
|
519
|
-
sw = Math.max(0, x1 - x0);
|
|
520
|
-
sh = Math.max(0, y1 - y0);
|
|
521
|
-
if (sw * sh > CACHE_LIMIT) {
|
|
522
|
-
sw = 0;
|
|
523
|
-
sh = 0;
|
|
524
|
-
}
|
|
525
|
-
if (siteCache.length < sw * sh)
|
|
526
|
-
siteCache = new Float64Array(sw * sh);
|
|
527
|
-
siteCache.fill(NaN, 0, sw * sh);
|
|
528
|
-
if (!input.kernels) {
|
|
529
|
-
// A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
|
|
530
|
-
if (sh > 0 && directRows < 2 * sh) {
|
|
531
|
-
let rows = 1;
|
|
532
|
-
while (rows < 2 * sh)
|
|
533
|
-
rows *= 2;
|
|
534
|
-
if (rows * W <= CACHE_LIMIT) {
|
|
535
|
-
directRows = rows;
|
|
536
|
-
directRing = new Float64Array(rows * W).fill(NaN);
|
|
537
|
-
directRowId = new Int32Array(rows).fill(-1);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
directOn = sh > 0 && directRows >= 2 * sh;
|
|
541
|
-
return;
|
|
542
|
-
}
|
|
543
|
-
for (const group of groups) {
|
|
544
|
-
group.cx0 = x0 + group.minIx - R;
|
|
545
|
-
group.cy0 = y0 + group.minIy - R;
|
|
546
|
-
group.cpw = sw === 0 ? 0 : sw + group.cw - 1;
|
|
547
|
-
group.cph = sh === 0 ? 0 : sh + group.ch - 1;
|
|
548
|
-
if (group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes) > CACHE_LIMIT) {
|
|
549
|
-
group.cpw = 0;
|
|
550
|
-
group.cph = 0;
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
// Chart materials whose every group caches the whole window: the filled path (bands). Otherwise the lazy window caches.
|
|
554
|
-
if (scene.kind === "materials" && sw > 0 && groups.every((group) => group.cpw > 0)) {
|
|
555
|
-
groups.forEach((group, g) => coverBand(g, group.cx0, group.cx0 + group.cpw, group.cy0, group.cph));
|
|
556
|
-
filled = true;
|
|
557
|
-
if (windowMaterial.length < sw * sh)
|
|
558
|
-
windowMaterial = new Int32Array(sw * sh);
|
|
559
|
-
// A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
|
|
560
|
-
if (ringRows < 2 * sh) {
|
|
561
|
-
let rows = 1;
|
|
562
|
-
while (rows < 2 * sh)
|
|
563
|
-
rows *= 2;
|
|
564
|
-
if (rows * W <= CACHE_LIMIT) {
|
|
565
|
-
ringRows = rows;
|
|
566
|
-
ring = new Float64Array(rows * W).fill(NaN);
|
|
567
|
-
ringMaterial = new Int32Array(rows * W);
|
|
568
|
-
ringRowId = new Int32Array(rows).fill(-1);
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
ringOn = ringRows >= 2 * sh;
|
|
572
|
-
return;
|
|
573
|
-
}
|
|
574
|
-
for (const group of groups) {
|
|
575
|
-
const n = group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes);
|
|
576
|
-
if (scene.kind === "materials") {
|
|
577
|
-
if (group.cache.length < n || !(group.cache instanceof Int32Array))
|
|
578
|
-
group.cache = new Int32Array(n);
|
|
579
|
-
group.cache.fill(-1, 0, n);
|
|
580
|
-
}
|
|
581
|
-
else {
|
|
582
|
-
if (group.cache.length < n || !(group.cache instanceof Float64Array))
|
|
583
|
-
group.cache = new Float64Array(n);
|
|
584
|
-
group.cache.fill(NaN, 0, n);
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
},
|
|
588
|
-
site(x, y) {
|
|
589
|
-
// The window's site cache first (with the ring: the sites read in this window, which its lookup count needs too).
|
|
590
|
-
const i = x - sx0, j = y - sy0, cached = i >= 0 && j >= 0 && i < sw && j < sh;
|
|
591
|
-
if (cached) {
|
|
592
|
-
const w = j * sw + i, v = siteCache[w];
|
|
593
|
-
if (v === v) {
|
|
594
|
-
if (ringOn)
|
|
595
|
-
lastMaterial = windowMaterial[w];
|
|
596
|
-
return v;
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
return siteMiss(x, y, i, j, cached);
|
|
600
|
-
},
|
|
601
|
-
pilot(u, v) {
|
|
602
|
-
lookups++;
|
|
603
|
-
return scene.kind === "materials" ? scene.materialAt(u, v) : scene.radianceAt(u, v, pilotNode);
|
|
604
|
-
}
|
|
605
|
-
};
|
|
606
|
-
}
|
|
43
|
+
export { createDisplayViewExpectedSignal } from "./expected-signal.js";
|
|
607
44
|
// ---------------------------------------------------------------------------------------------------------------
|
|
608
45
|
// Normal-distribution helpers.
|
|
609
46
|
/** Standard normal density. */
|
|
@@ -884,6 +321,13 @@ function correctionStates(key) {
|
|
|
884
321
|
const MEMO_SIZE = 37;
|
|
885
322
|
/** Most real pixels a display pixel's footprint may have for the per-real-pixel memo (#392); larger footprints rarely repeat a real pixel. */
|
|
886
323
|
const PIXEL_MEMO_LIMIT = 1 << 16;
|
|
324
|
+
/**
|
|
325
|
+
* Most real pixels a footprint may hold for the exact area mean (#393). Measured on 8-row strips of a 24 MP view (Siemens star
|
|
326
|
+
* and dead leaves, one thread): the exact path matched the stratified estimator's time within run-to-run spread up to 25 real
|
|
327
|
+
* pixels per display pixel and was slower above (1.2× at 36, 1.4–1.5× at 64, 1.5–2.4× at 144), because every real pixel is
|
|
328
|
+
* evaluated while the stratified estimator takes a quarter of its samples per axis where the pilot finds no detail.
|
|
329
|
+
*/
|
|
330
|
+
export const EXACT_AREA_MEAN_MAX_PIXELS = 25;
|
|
887
331
|
/** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
|
|
888
332
|
function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
|
|
889
333
|
/** Renders the display view (docs/DISPLAY_VIEW.md). */
|
|
@@ -891,6 +335,21 @@ export async function renderDisplayView(setup) {
|
|
|
891
335
|
const { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth, viewHeight, region, seed, reachSites } = setup;
|
|
892
336
|
const sx = frameWidth / viewWidth, sy = frameHeight / viewHeight, area = sx * sy, g = 2 / frameWidth;
|
|
893
337
|
const fullK = setup.samplesPerAxis, uniformK = Math.max(1, Math.round(fullK / 4));
|
|
338
|
+
// Exact area mean (#393): where a footprint holds at most EXACT_AREA_MEAN_MAX_PIXELS real pixels and at most K², every real
|
|
339
|
+
// pixel it touches is evaluated once with its exact overlap weight a_j = w_j / A, so the expected value is D_P = Σ_j a_j·E[L_j]
|
|
340
|
+
// with no sampling error at no more cost than sampling; coarser views keep the stratified estimator, whose cost does not
|
|
341
|
+
// grow with the footprint.
|
|
342
|
+
const exactView = area <= Math.min(EXACT_AREA_MEAN_MAX_PIXELS, fullK * fullK);
|
|
343
|
+
const columnOverlaps = exactView ? Array.from({ length: region.width }, (_, i) => overlaps((region.x + i) * sx, (region.x + i + 1) * sx, frameWidth)) : [];
|
|
344
|
+
// On the exact path a real pixel's recorded terms are shared by the 2–4 display pixels whose footprints touch it: they are
|
|
345
|
+
// kept for the region's native columns in a ring of native rows (a power of two, at least twice a footprint's height plus
|
|
346
|
+
// one), valid while a slot's stamp is its native row + 1.
|
|
347
|
+
const exactX0 = Math.floor(region.x * sx) + margin, exactWidth = Math.min(frameWidth - 1, Math.ceil((region.x + region.width) * sx) - 1) + margin - exactX0 + 1;
|
|
348
|
+
let exactRows = 1;
|
|
349
|
+
while (exactRows < 2 * (Math.ceil(sy) + 1))
|
|
350
|
+
exactRows *= 2;
|
|
351
|
+
const exactMemo = exactView && exactRows * exactWidth <= PIXEL_MEMO_LIMIT * 4;
|
|
352
|
+
const exactStamp = new Int32Array(exactMemo ? exactRows * exactWidth : 0), exactRecords = new Float64Array(exactMemo ? exactRows * exactWidth * MEMO_SIZE : 0);
|
|
894
353
|
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
895
354
|
const pixelCount = region.width * region.height;
|
|
896
355
|
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
@@ -1105,7 +564,7 @@ export async function renderDisplayView(setup) {
|
|
|
1105
564
|
}
|
|
1106
565
|
let siteCount = 0, sampleCount = 0, detail = 0, uniform = 0, lastYield = Date.now();
|
|
1107
566
|
/** Recorded terms of samples whose sites all read one chart material, by material·4 + CFA phase (layout: MEMO_SIZE). */
|
|
1108
|
-
const sampleMemo = new Map();
|
|
567
|
+
const sampleMemo = new Map(), recordScratch = new Float64Array(MEMO_SIZE);
|
|
1109
568
|
// Per-real-pixel memo (#392): a sample's recorded terms depend only on its real pixel (its sites' expected electrons and its
|
|
1110
569
|
// CFA phase), not on the display pixel or the sample, so a later sample of the same real pixel in the same display pixel
|
|
1111
570
|
// replays them (the same values added to the same sums in the same order, as the one-material memo does). Slots cover the
|
|
@@ -1137,7 +596,8 @@ export async function renderDisplayView(setup) {
|
|
|
1137
596
|
const ringWidth = region.width + 2, flagRows = [];
|
|
1138
597
|
const pilotRow = (vy) => {
|
|
1139
598
|
const row = new Uint8Array(ringWidth);
|
|
1140
|
-
|
|
599
|
+
// The exact area mean needs no pilot: every pixel evaluates its whole footprint.
|
|
600
|
+
if (exactView || vy < 0 || vy >= viewHeight)
|
|
1141
601
|
return row.fill(1);
|
|
1142
602
|
for (let i = 0; i < ringWidth; i++) {
|
|
1143
603
|
const vx = region.x - 1 + i;
|
|
@@ -1147,7 +607,7 @@ export async function renderDisplayView(setup) {
|
|
|
1147
607
|
};
|
|
1148
608
|
flagRows.push(pilotRow(region.y - 1), pilotRow(region.y));
|
|
1149
609
|
for (let ry = 0; ry < region.height; ry++) {
|
|
1150
|
-
const vy = region.y + ry, rows = axisSums(vy * sy, (vy + 1) * sy, frameHeight, margin);
|
|
610
|
+
const vy = region.y + ry, rows = axisSums(vy * sy, (vy + 1) * sy, frameHeight, margin), rowOverlaps = exactView ? overlaps(vy * sy, (vy + 1) * sy, frameHeight) : undefined;
|
|
1151
611
|
flagRows.push(pilotRow(vy + 1));
|
|
1152
612
|
if (flagRows.length > 3)
|
|
1153
613
|
flagRows.shift();
|
|
@@ -1162,15 +622,19 @@ export async function renderDisplayView(setup) {
|
|
|
1162
622
|
break;
|
|
1163
623
|
}
|
|
1164
624
|
const K = same ? uniformK : fullK, Q = K * K;
|
|
1165
|
-
|
|
625
|
+
// On the exact path every pixel counts as detailed (its whole footprint is evaluated).
|
|
626
|
+
if (exactView)
|
|
627
|
+
detail++;
|
|
628
|
+
else if (same)
|
|
1166
629
|
uniform++;
|
|
1167
630
|
else
|
|
1168
631
|
detail++;
|
|
632
|
+
const cols = exactView ? columnOverlaps[rx] : undefined, nx = cols ? cols.w.length : 0, sampleTotal = exactView ? nx * rowOverlaps.w.length : Q;
|
|
1169
633
|
setup.signal.window(Math.floor(vx * sx) + margin - windowReach, Math.floor(vy * sy) + margin - windowReach, Math.ceil((vx + 1) * sx) + margin + windowReach, Math.ceil((vy + 1) * sy) + margin + windowReach);
|
|
1170
634
|
const jitter = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_JITTER_STREAM_ID)), tracking = setup.signal.tracksMaterial;
|
|
1171
635
|
const mx0 = Math.floor(vx * sx) + margin, my0 = Math.floor(vy * sy) + margin;
|
|
1172
636
|
const mw = Math.min(frameWidth - 1, Math.ceil((vx + 1) * sx)) + margin - mx0 + 1, mh = Math.min(frameHeight - 1, Math.ceil((vy + 1) * sy)) + margin - my0 + 1;
|
|
1173
|
-
const pixelMemo = setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
|
|
637
|
+
const pixelMemo = !exactView && setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
|
|
1174
638
|
if (pixelMemo) {
|
|
1175
639
|
if (pixelStamp.length < mw * mh) {
|
|
1176
640
|
pixelStamp = new Int32Array(mw * mh);
|
|
@@ -1180,311 +644,330 @@ export async function renderDisplayView(setup) {
|
|
|
1180
644
|
pixelGeneration++;
|
|
1181
645
|
}
|
|
1182
646
|
let s0 = 0, s1 = 0, s2 = 0, c00 = 0, c01 = 0, c02 = 0, c11 = 0, c12 = 0, c22 = 0, r0 = 0, r1 = 0, r2 = 0, pixelVariance = 0, corrected = false;
|
|
1183
|
-
for (let
|
|
1184
|
-
for (
|
|
647
|
+
for (let n = 0; n < sampleTotal; n++) {
|
|
648
|
+
// A sample's weights: 1 for the stratified estimator (its sums are divided by Q below); a_j for the expected value and
|
|
649
|
+
// linear covariance and a_j² for the independent residual on the exact path. Multiplying by 1 leaves every value's bits.
|
|
650
|
+
let x, y, wE = 1, wR = 1;
|
|
651
|
+
if (cols) {
|
|
652
|
+
const i = n % nx, j = (n - i) / nx, aj = cols.w[i] * rowOverlaps.w[j] / area;
|
|
653
|
+
x = cols.first + i + margin;
|
|
654
|
+
y = rowOverlaps.first + j + margin;
|
|
655
|
+
wE = aj;
|
|
656
|
+
wR = aj * aj;
|
|
657
|
+
}
|
|
658
|
+
else {
|
|
659
|
+
const a = n % K, b = (n - a) / K;
|
|
1185
660
|
const fx = (vx + (a + jitter.next()) / K) * sx, fy = (vy + (b + jitter.next()) / K) * sy;
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
const
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
siteMean[slot] = mean;
|
|
1227
|
-
siteVar[slot] = variance;
|
|
1228
|
-
}
|
|
1229
|
-
else {
|
|
1230
|
-
upperClip(mean, variance, 1, clip);
|
|
1231
|
-
siteMean[slot] = clip[0];
|
|
1232
|
-
siteVar[slot] = clip[1];
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
if (!tracking)
|
|
1236
|
-
material = -1;
|
|
1237
|
-
// A sample whose sites all read one chart material only has the statistics of every other such sample of that material
|
|
1238
|
-
// and CFA phase: replay its recorded terms (the same values added to the same sums in the same order).
|
|
1239
|
-
const memoKey = !memo && material >= 0 ? material * 4 + phase : -1;
|
|
1240
|
-
if (memoKey >= 0) {
|
|
1241
|
-
memo = sampleMemo.get(memoKey);
|
|
1242
|
-
if (memo && pk >= 0) {
|
|
1243
|
-
pixelRecords.set(memo, pk * MEMO_SIZE);
|
|
1244
|
-
pixelStamp[pk] = pixelGeneration;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
const cs = candidateSlots[phase], cw = candidateWeights[phase], count = cs.length, share = 1 / count;
|
|
1248
|
-
if (memo) {
|
|
1249
|
-
for (let d = 0; d < count; d++) {
|
|
1250
|
-
for (let c = 0; c < 3; c++) {
|
|
1251
|
-
const o = mo + 3 + (d * 3 + c) * 3, a0 = memo[o], a1 = memo[o + 1], a2 = memo[o + 2], wgt = share * B[c] * memo[mo + c];
|
|
1252
|
-
c00 += wgt * a0 * a0;
|
|
1253
|
-
c01 += wgt * a0 * a1;
|
|
1254
|
-
c02 += wgt * a0 * a2;
|
|
1255
|
-
c11 += wgt * a1 * a1;
|
|
1256
|
-
c12 += wgt * a1 * a2;
|
|
1257
|
-
c22 += wgt * a2 * a2;
|
|
1258
|
-
}
|
|
1259
|
-
r0 += memo[mo + 21 + d * 3];
|
|
1260
|
-
r1 += memo[mo + 22 + d * 3];
|
|
1261
|
-
r2 += memo[mo + 23 + d * 3];
|
|
661
|
+
x = Math.min(frameWidth - 1, Math.floor(fx)) + margin;
|
|
662
|
+
y = Math.min(frameHeight - 1, Math.floor(fy)) + margin;
|
|
663
|
+
}
|
|
664
|
+
const phase = (y & 1) * 2 + (x & 1), list = phaseList[phase];
|
|
665
|
+
let material = -2;
|
|
666
|
+
const px = x - mx0, py = y - my0;
|
|
667
|
+
const inside = x >= reach && y >= reach && x + reach < nativeWidth && y + reach < nativeHeight;
|
|
668
|
+
const stamps = exactMemo ? exactStamp : pixelStamp, records = exactMemo ? exactRecords : pixelRecords, stamp = exactMemo ? y + 1 : pixelGeneration;
|
|
669
|
+
const pk = exactMemo ? (inside && x >= exactX0 && x - exactX0 < exactWidth ? (y & (exactRows - 1)) * exactWidth + x - exactX0 : -1)
|
|
670
|
+
: pixelMemo && px >= 0 && py >= 0 && px < mw && py < mh && inside ? py * mw + px : -1;
|
|
671
|
+
let memo, mo = 0;
|
|
672
|
+
if (pk >= 0 && stamps[pk] === stamp) {
|
|
673
|
+
memo = records;
|
|
674
|
+
mo = pk * MEMO_SIZE;
|
|
675
|
+
siteCount += list.length;
|
|
676
|
+
}
|
|
677
|
+
// The expected electrons of the sites the demosaic reads (sites outside the raster, at mirror borders, are read
|
|
678
|
+
// reflect-101); where the signal tracks it, whether they all read one chart material, with the statistics below only
|
|
679
|
+
// when the sample is not replayed; otherwise the statistics at once.
|
|
680
|
+
if (!memo)
|
|
681
|
+
for (let li = 0; li < list.length; li++) {
|
|
682
|
+
const slot = list[li];
|
|
683
|
+
siteCount++;
|
|
684
|
+
let sxs = x + (slot % side) - reach, sys = y + ((slot / side) | 0) - reach;
|
|
685
|
+
if (sxs < 0)
|
|
686
|
+
sxs = -sxs;
|
|
687
|
+
else if (sxs >= nativeWidth)
|
|
688
|
+
sxs = 2 * nativeWidth - 2 - sxs;
|
|
689
|
+
if (sys < 0)
|
|
690
|
+
sys = -sys;
|
|
691
|
+
else if (sys >= nativeHeight)
|
|
692
|
+
sys = 2 * nativeHeight - 2 - sys;
|
|
693
|
+
const c = (sys & 1) === 0 ? ((sxs & 1) === 0 ? 0 : 1) : ((sxs & 1) === 0 ? 1 : 2);
|
|
694
|
+
const e = setup.signal.site(sxs, sys) + dark[c];
|
|
695
|
+
siteChannel[slot] = c;
|
|
696
|
+
if (tracking) {
|
|
697
|
+
siteElectrons[slot] = e;
|
|
698
|
+
const own = setup.signal.uniformMaterial;
|
|
699
|
+
material = material === -2 || material === own ? own : -1;
|
|
700
|
+
continue;
|
|
1262
701
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
if (
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
r2 += memo[mo + 32];
|
|
702
|
+
// As siteStatistics, written out here for the untracked (Speed) path.
|
|
703
|
+
const kc = k[c], mean = kc * e, variance = kc * kc * e + c0[c], gap = 1 - mean;
|
|
704
|
+
siteThird[slot] = kc * kc * kc * e;
|
|
705
|
+
if (gap > 0 && gap * gap > reach2 * variance) {
|
|
706
|
+
siteMean[slot] = mean;
|
|
707
|
+
siteVar[slot] = variance;
|
|
1270
708
|
}
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
corrected = true;
|
|
709
|
+
else {
|
|
710
|
+
upperClip(mean, variance, 1, clip);
|
|
711
|
+
siteMean[slot] = clip[0];
|
|
712
|
+
siteVar[slot] = clip[1];
|
|
1276
713
|
}
|
|
1277
|
-
continue;
|
|
1278
714
|
}
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
const sl = slots[c], wt = weights[c];
|
|
1290
|
-
let variance = 0;
|
|
1291
|
-
for (let t = 0; t < sl.length; t++)
|
|
1292
|
-
variance += wt[t] * wt[t] * siteVar[sl[t]];
|
|
1293
|
-
csv[c] = variance / w2[c];
|
|
715
|
+
if (!tracking)
|
|
716
|
+
material = -1;
|
|
717
|
+
// A sample whose sites all read one chart material only has the statistics of every other such sample of that material
|
|
718
|
+
// and CFA phase: replay its recorded terms (the same values added to the same sums in the same order).
|
|
719
|
+
const memoKey = !memo && material >= 0 ? material * 4 + phase : -1;
|
|
720
|
+
if (memoKey >= 0) {
|
|
721
|
+
memo = sampleMemo.get(memoKey);
|
|
722
|
+
if (memo && pk >= 0) {
|
|
723
|
+
records.set(memo, pk * MEMO_SIZE);
|
|
724
|
+
stamps[pk] = stamp;
|
|
1294
725
|
}
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
let e0 = 0, e1 = 0, e2 = 0;
|
|
726
|
+
}
|
|
727
|
+
const cs = candidateSlots[phase], cw = candidateWeights[phase], count = cs.length, share = 1 / count;
|
|
728
|
+
if (memo) {
|
|
1299
729
|
for (let d = 0; d < count; d++) {
|
|
1300
730
|
for (let c = 0; c < 3; c++) {
|
|
1301
|
-
const
|
|
1302
|
-
let mean = 0;
|
|
1303
|
-
for (let t = 0; t < sl.length; t++)
|
|
1304
|
-
mean += wt[t] * siteMean[sl[t]];
|
|
1305
|
-
cm[c] = mean;
|
|
1306
|
-
cmAverage[c] = (d === 0 ? 0 : cmAverage[c]) + mean * share;
|
|
1307
|
-
}
|
|
1308
|
-
// Covariance of channels i, j: Σ over the sites of w_i·w_j·Var(site), with the kernels as dense weights over the window.
|
|
1309
|
-
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
1310
|
-
let v00 = 0, v01 = 0, v02 = 0, v11 = 0, v12 = 0, v22 = 0;
|
|
1311
|
-
for (let li = 0; li < list.length; li++) {
|
|
1312
|
-
const slot = list[li], v = siteVar[slot], w0 = dense[slot], w1 = dense[slotsCount + slot], w2 = dense[2 * slotsCount + slot];
|
|
1313
|
-
v00 += w0 * w0 * v;
|
|
1314
|
-
v01 += w0 * w1 * v;
|
|
1315
|
-
v02 += w0 * w2 * v;
|
|
1316
|
-
v11 += w1 * w1 * v;
|
|
1317
|
-
v12 += w1 * w2 * v;
|
|
1318
|
-
v22 += w2 * w2 * v;
|
|
1319
|
-
}
|
|
1320
|
-
C[0] = v00;
|
|
1321
|
-
C[1] = v01;
|
|
1322
|
-
C[2] = v02;
|
|
1323
|
-
C[3] = v01;
|
|
1324
|
-
C[4] = v11;
|
|
1325
|
-
C[5] = v12;
|
|
1326
|
-
C[6] = v02;
|
|
1327
|
-
C[7] = v12;
|
|
1328
|
-
C[8] = v22;
|
|
1329
|
-
// White balance and the highlight clip at the smallest gain (per channel; covariances scaled by the expected slopes).
|
|
1330
|
-
for (let c = 0; c < 3; c++) {
|
|
1331
|
-
const mean = cm[c], variance = C[c * 4];
|
|
1332
|
-
cv[c] = variance;
|
|
1333
|
-
const gc = c === 0 ? g0 : c === 1 ? g1 : g2, um = gc * mean, uv = gc * gc * variance, gap = L - um;
|
|
1334
|
-
if (gap > 0 && gap * gap > reach2 * uv) {
|
|
1335
|
-
bm[c] = um;
|
|
1336
|
-
bv[c] = uv;
|
|
1337
|
-
bs[c] = gc;
|
|
1338
|
-
}
|
|
1339
|
-
else {
|
|
1340
|
-
upperClip(um, uv, L, clip);
|
|
1341
|
-
bm[c] = clip[0];
|
|
1342
|
-
bv[c] = clip[1];
|
|
1343
|
-
bs[c] = gc * clip[2];
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
// Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).
|
|
1347
|
-
for (let comp = 0; comp < 3; comp++) {
|
|
1348
|
-
const r0m = comp === 0 ? m00 : comp === 1 ? m10 : m20, r1m = comp === 0 ? m01 : comp === 1 ? m11 : m21, r2m = comp === 0 ? m02 : comp === 1 ? m12 : m22;
|
|
1349
|
-
const mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];
|
|
1350
|
-
const variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2] +
|
|
1351
|
-
2 * (r0m * r1m * bs[0] * bs[1] * C[1] + r0m * r2m * bs[0] * bs[2] * C[2] + r1m * r2m * bs[1] * bs[2] * C[5]);
|
|
1352
|
-
if (!reinhard && mean > 0 && mean < 1 && mean * mean > reach2 * variance && (1 - mean) * (1 - mean) > reach2 * variance) {
|
|
1353
|
-
hm[comp] = mean;
|
|
1354
|
-
hv[comp] = variance;
|
|
1355
|
-
hs[comp] = 1;
|
|
1356
|
-
}
|
|
1357
|
-
else if (reinhard) {
|
|
1358
|
-
reinhardMoments(mean, variance, clip);
|
|
1359
|
-
hm[comp] = clip[0];
|
|
1360
|
-
hv[comp] = clip[1];
|
|
1361
|
-
hs[comp] = clip[2];
|
|
1362
|
-
}
|
|
1363
|
-
else {
|
|
1364
|
-
// The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
|
|
1365
|
-
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
1366
|
-
let third = 0;
|
|
1367
|
-
for (let li = 0; li < list.length; li++) {
|
|
1368
|
-
const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
|
|
1369
|
-
third += coefficient * coefficient * coefficient * siteThird[slot];
|
|
1370
|
-
}
|
|
1371
|
-
unitClip(mean, variance, clip, variance > 0 ? third / (variance * Math.sqrt(variance)) : 0);
|
|
1372
|
-
hm[comp] = clip[0];
|
|
1373
|
-
hv[comp] = clip[1];
|
|
1374
|
-
hs[comp] = clip[2];
|
|
1375
|
-
}
|
|
1376
|
-
}
|
|
1377
|
-
e0 += share * hm[0];
|
|
1378
|
-
e1 += share * hm[1];
|
|
1379
|
-
e2 += share * hm[2];
|
|
1380
|
-
// Linear (Stein) part: a_c = J ∘ M[:, c]·Γ_c, accumulated with B_c·σ_c² (site variance of channel c); the pixel's own
|
|
1381
|
-
// linear variance (aᵀ·C·a, full channel covariance) for the residual.
|
|
1382
|
-
let l0 = 0, l1 = 0, l2 = 0;
|
|
1383
|
-
for (let c = 0; c < 3; c++) {
|
|
1384
|
-
const a0 = hs[0] * (c === 0 ? m00 : c === 1 ? m01 : m02) * bs[c], a1 = hs[1] * (c === 0 ? m10 : c === 1 ? m11 : m12) * bs[c], a2 = hs[2] * (c === 0 ? m20 : c === 1 ? m21 : m22) * bs[c];
|
|
1385
|
-
const wgt = share * B[c] * csv[c];
|
|
731
|
+
const o = mo + 3 + (d * 3 + c) * 3, a0 = memo[o], a1 = memo[o + 1], a2 = memo[o + 2], wgt = wE * share * B[c] * memo[mo + c];
|
|
1386
732
|
c00 += wgt * a0 * a0;
|
|
1387
733
|
c01 += wgt * a0 * a1;
|
|
1388
734
|
c02 += wgt * a0 * a2;
|
|
1389
735
|
c11 += wgt * a1 * a1;
|
|
1390
736
|
c12 += wgt * a1 * a2;
|
|
1391
737
|
c22 += wgt * a2 * a2;
|
|
1392
|
-
an[c] = a0;
|
|
1393
|
-
an[3 + c] = a1;
|
|
1394
|
-
an[6 + c] = a2;
|
|
1395
|
-
if (record) {
|
|
1396
|
-
const o = 3 + (d * 3 + c) * 3;
|
|
1397
|
-
record[o] = a0;
|
|
1398
|
-
record[o + 1] = a1;
|
|
1399
|
-
record[o + 2] = a2;
|
|
1400
|
-
}
|
|
1401
738
|
}
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
l0 += an[i] * an[j] * cij;
|
|
1406
|
-
l1 += an[3 + i] * an[3 + j] * cij;
|
|
1407
|
-
l2 += an[6 + i] * an[6 + j] * cij;
|
|
1408
|
-
}
|
|
1409
|
-
const t0 = share * Math.max(0, hv[0] - l0), t1 = share * Math.max(0, hv[1] - l1), t2 = share * Math.max(0, hv[2] - l2);
|
|
1410
|
-
r0 += t0;
|
|
1411
|
-
r1 += t1;
|
|
1412
|
-
r2 += t2;
|
|
1413
|
-
if (record) {
|
|
1414
|
-
record[21 + d * 3] = t0;
|
|
1415
|
-
record[22 + d * 3] = t1;
|
|
1416
|
-
record[23 + d * 3] = t2;
|
|
1417
|
-
}
|
|
1418
|
-
pixelVariance = Math.max(pixelVariance, hv[0], hv[1], hv[2]);
|
|
1419
|
-
}
|
|
1420
|
-
s0 += e0;
|
|
1421
|
-
s1 += e1;
|
|
1422
|
-
s2 += e2;
|
|
1423
|
-
if (record) {
|
|
1424
|
-
record[0] = csv[0];
|
|
1425
|
-
record[1] = csv[1];
|
|
1426
|
-
record[2] = csv[2];
|
|
1427
|
-
record[27] = e0;
|
|
1428
|
-
record[28] = e1;
|
|
1429
|
-
record[29] = e2;
|
|
739
|
+
r0 += wR * memo[mo + 21 + d * 3];
|
|
740
|
+
r1 += wR * memo[mo + 22 + d * 3];
|
|
741
|
+
r2 += wR * memo[mo + 23 + d * 3];
|
|
1430
742
|
}
|
|
743
|
+
s0 += wE * memo[mo + 27];
|
|
744
|
+
s1 += wE * memo[mo + 28];
|
|
745
|
+
s2 += wE * memo[mo + 29];
|
|
1431
746
|
if (count === 2) {
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
747
|
+
r0 += wR * memo[mo + 30];
|
|
748
|
+
r1 += wR * memo[mo + 31];
|
|
749
|
+
r2 += wR * memo[mo + 32];
|
|
750
|
+
}
|
|
751
|
+
if (memo[mo + 33] === 1) {
|
|
752
|
+
s0 += wE * memo[mo + 34];
|
|
753
|
+
s1 += wE * memo[mo + 35];
|
|
754
|
+
s2 += wE * memo[mo + 36];
|
|
755
|
+
corrected = true;
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
// The terms are written where they are kept: the real pixel's memo slot, or (for the first sample of a one-material key)
|
|
760
|
+
// a scratch buffer that is then copied; no typed-array view per sample.
|
|
761
|
+
const rt = memoKey >= 0 ? recordScratch.fill(0) : pk >= 0 ? records.fill(0, pk * MEMO_SIZE, (pk + 1) * MEMO_SIZE) : undefined, ro = memoKey < 0 && pk >= 0 ? pk * MEMO_SIZE : 0;
|
|
762
|
+
if (tracking)
|
|
763
|
+
for (let li = 0; li < list.length; li++) {
|
|
764
|
+
const slot = list[li];
|
|
765
|
+
siteStatistics(slot, siteChannel[slot], siteElectrons[slot]);
|
|
766
|
+
}
|
|
767
|
+
// Site variance of each channel through the bilinear kernels (for B_c), then the demosaic's own pixel statistics: the
|
|
768
|
+
// mean and covariance of its camera channels, averaged over its candidates (AHD picks one per pixel).
|
|
769
|
+
const slots = phaseSlots[phase], weights = phaseWeights[phase], w2 = phaseW2[phase];
|
|
770
|
+
for (let c = 0; c < 3; c++) {
|
|
771
|
+
const sl = slots[c], wt = weights[c];
|
|
772
|
+
let variance = 0;
|
|
773
|
+
for (let t = 0; t < sl.length; t++)
|
|
774
|
+
variance += wt[t] * wt[t] * siteVar[sl[t]];
|
|
775
|
+
csv[c] = variance / w2[c];
|
|
776
|
+
}
|
|
777
|
+
// Each candidate of the demosaic (one for bilinear; AHD's horizontal and vertical ones, of which AHD picks one per pixel)
|
|
778
|
+
// is propagated on its own: camera-channel mean and covariance, white balance and highlight clip, colour matrix and
|
|
779
|
+
// rendering; the pixel's statistics are their average.
|
|
780
|
+
let e0 = 0, e1 = 0, e2 = 0;
|
|
781
|
+
for (let d = 0; d < count; d++) {
|
|
782
|
+
for (let c = 0; c < 3; c++) {
|
|
783
|
+
const sl = cs[d][c], wt = cw[d][c];
|
|
784
|
+
let mean = 0;
|
|
785
|
+
for (let t = 0; t < sl.length; t++)
|
|
786
|
+
mean += wt[t] * siteMean[sl[t]];
|
|
787
|
+
cm[c] = mean;
|
|
788
|
+
cmAverage[c] = (d === 0 ? 0 : cmAverage[c]) + mean * share;
|
|
789
|
+
}
|
|
790
|
+
// Covariance of channels i, j: Σ over the sites of w_i·w_j·Var(site), with the kernels as dense weights over the window.
|
|
791
|
+
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
792
|
+
let v00 = 0, v01 = 0, v02 = 0, v11 = 0, v12 = 0, v22 = 0;
|
|
793
|
+
for (let li = 0; li < list.length; li++) {
|
|
794
|
+
const slot = list[li], v = siteVar[slot], w0 = dense[slot], w1 = dense[slotsCount + slot], w2 = dense[2 * slotsCount + slot];
|
|
795
|
+
v00 += w0 * w0 * v;
|
|
796
|
+
v01 += w0 * w1 * v;
|
|
797
|
+
v02 += w0 * w2 * v;
|
|
798
|
+
v11 += w1 * w1 * v;
|
|
799
|
+
v12 += w1 * w2 * v;
|
|
800
|
+
v22 += w2 * w2 * v;
|
|
801
|
+
}
|
|
802
|
+
C[0] = v00;
|
|
803
|
+
C[1] = v01;
|
|
804
|
+
C[2] = v02;
|
|
805
|
+
C[3] = v01;
|
|
806
|
+
C[4] = v11;
|
|
807
|
+
C[5] = v12;
|
|
808
|
+
C[6] = v02;
|
|
809
|
+
C[7] = v12;
|
|
810
|
+
C[8] = v22;
|
|
811
|
+
// White balance and the highlight clip at the smallest gain (per channel; covariances scaled by the expected slopes).
|
|
812
|
+
for (let c = 0; c < 3; c++) {
|
|
813
|
+
const mean = cm[c], variance = C[c * 4];
|
|
814
|
+
cv[c] = variance;
|
|
815
|
+
const gc = c === 0 ? g0 : c === 1 ? g1 : g2, um = gc * mean, uv = gc * gc * variance, gap = L - um;
|
|
816
|
+
if (gap > 0 && gap * gap > reach2 * uv) {
|
|
817
|
+
bm[c] = um;
|
|
818
|
+
bv[c] = uv;
|
|
819
|
+
bs[c] = gc;
|
|
1443
820
|
}
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
record[31] = x1 / 4;
|
|
1450
|
-
record[32] = x2 / 4;
|
|
821
|
+
else {
|
|
822
|
+
upperClip(um, uv, L, clip);
|
|
823
|
+
bm[c] = clip[0];
|
|
824
|
+
bv[c] = clip[1];
|
|
825
|
+
bs[c] = gc * clip[2];
|
|
1451
826
|
}
|
|
1452
827
|
}
|
|
1453
|
-
//
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
if (
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
828
|
+
// Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).
|
|
829
|
+
for (let comp = 0; comp < 3; comp++) {
|
|
830
|
+
const r0m = comp === 0 ? m00 : comp === 1 ? m10 : m20, r1m = comp === 0 ? m01 : comp === 1 ? m11 : m21, r2m = comp === 0 ? m02 : comp === 1 ? m12 : m22;
|
|
831
|
+
const mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];
|
|
832
|
+
const variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2] +
|
|
833
|
+
2 * (r0m * r1m * bs[0] * bs[1] * C[1] + r0m * r2m * bs[0] * bs[2] * C[2] + r1m * r2m * bs[1] * bs[2] * C[5]);
|
|
834
|
+
if (!reinhard && mean > 0 && mean < 1 && mean * mean > reach2 * variance && (1 - mean) * (1 - mean) > reach2 * variance) {
|
|
835
|
+
hm[comp] = mean;
|
|
836
|
+
hv[comp] = variance;
|
|
837
|
+
hs[comp] = 1;
|
|
838
|
+
}
|
|
839
|
+
else if (reinhard) {
|
|
840
|
+
reinhardMoments(mean, variance, clip);
|
|
841
|
+
hm[comp] = clip[0];
|
|
842
|
+
hv[comp] = clip[1];
|
|
843
|
+
hs[comp] = clip[2];
|
|
844
|
+
}
|
|
845
|
+
else {
|
|
846
|
+
// The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
|
|
847
|
+
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
848
|
+
let third = 0;
|
|
849
|
+
for (let li = 0; li < list.length; li++) {
|
|
850
|
+
const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
|
|
851
|
+
third += coefficient * coefficient * coefficient * siteThird[slot];
|
|
852
|
+
}
|
|
853
|
+
unitClip(mean, variance, clip, variance > 0 ? third / (variance * Math.sqrt(variance)) : 0);
|
|
854
|
+
hm[comp] = clip[0];
|
|
855
|
+
hv[comp] = clip[1];
|
|
856
|
+
hs[comp] = clip[2];
|
|
1469
857
|
}
|
|
1470
858
|
}
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
859
|
+
e0 += share * hm[0];
|
|
860
|
+
e1 += share * hm[1];
|
|
861
|
+
e2 += share * hm[2];
|
|
862
|
+
// Linear (Stein) part: a_c = J ∘ M[:, c]·Γ_c, accumulated with B_c·σ_c² (site variance of channel c); the pixel's own
|
|
863
|
+
// linear variance (aᵀ·C·a, full channel covariance) for the residual.
|
|
864
|
+
let l0 = 0, l1 = 0, l2 = 0;
|
|
865
|
+
for (let c = 0; c < 3; c++) {
|
|
866
|
+
const a0 = hs[0] * (c === 0 ? m00 : c === 1 ? m01 : m02) * bs[c], a1 = hs[1] * (c === 0 ? m10 : c === 1 ? m11 : m12) * bs[c], a2 = hs[2] * (c === 0 ? m20 : c === 1 ? m21 : m22) * bs[c];
|
|
867
|
+
const wgt = wE * share * B[c] * csv[c];
|
|
868
|
+
c00 += wgt * a0 * a0;
|
|
869
|
+
c01 += wgt * a0 * a1;
|
|
870
|
+
c02 += wgt * a0 * a2;
|
|
871
|
+
c11 += wgt * a1 * a1;
|
|
872
|
+
c12 += wgt * a1 * a2;
|
|
873
|
+
c22 += wgt * a2 * a2;
|
|
874
|
+
an[c] = a0;
|
|
875
|
+
an[3 + c] = a1;
|
|
876
|
+
an[6 + c] = a2;
|
|
877
|
+
if (rt) {
|
|
878
|
+
const o = 3 + (d * 3 + c) * 3;
|
|
879
|
+
rt[ro + o] = a0;
|
|
880
|
+
rt[ro + o + 1] = a1;
|
|
881
|
+
rt[ro + o + 2] = a2;
|
|
882
|
+
}
|
|
1475
883
|
}
|
|
884
|
+
for (let i = 0; i < 3; i++)
|
|
885
|
+
for (let j = 0; j < 3; j++) {
|
|
886
|
+
const cij = C[i * 3 + j];
|
|
887
|
+
l0 += an[i] * an[j] * cij;
|
|
888
|
+
l1 += an[3 + i] * an[3 + j] * cij;
|
|
889
|
+
l2 += an[6 + i] * an[6 + j] * cij;
|
|
890
|
+
}
|
|
891
|
+
const t0 = share * Math.max(0, hv[0] - l0), t1 = share * Math.max(0, hv[1] - l1), t2 = share * Math.max(0, hv[2] - l2);
|
|
892
|
+
r0 += wR * t0;
|
|
893
|
+
r1 += wR * t1;
|
|
894
|
+
r2 += wR * t2;
|
|
895
|
+
if (rt) {
|
|
896
|
+
rt[ro + 21 + d * 3] = t0;
|
|
897
|
+
rt[ro + 22 + d * 3] = t1;
|
|
898
|
+
rt[ro + 23 + d * 3] = t2;
|
|
899
|
+
}
|
|
900
|
+
pixelVariance = Math.max(pixelVariance, hv[0], hv[1], hv[2]);
|
|
901
|
+
}
|
|
902
|
+
s0 += wE * e0;
|
|
903
|
+
s1 += wE * e1;
|
|
904
|
+
s2 += wE * e2;
|
|
905
|
+
if (rt) {
|
|
906
|
+
rt[ro + 0] = csv[0];
|
|
907
|
+
rt[ro + 1] = csv[1];
|
|
908
|
+
rt[ro + 2] = csv[2];
|
|
909
|
+
rt[ro + 27] = e0;
|
|
910
|
+
rt[ro + 28] = e1;
|
|
911
|
+
rt[ro + 29] = e2;
|
|
912
|
+
}
|
|
913
|
+
if (count === 2) {
|
|
914
|
+
// AHD's per-pixel choice between the candidates adds independent variance: with a noise-driven choice each pixel
|
|
915
|
+
// deviates from the candidates' average by ±½(H − V), variance ¼·Var(H − V) (linear in the sites, at the last
|
|
916
|
+
// candidate's slopes).
|
|
917
|
+
const dh = candidateDense[phase][0], dv = candidateDense[phase][1], list = phaseList[phase];
|
|
918
|
+
let x0 = 0, x1 = 0, x2 = 0;
|
|
919
|
+
for (let li = 0; li < list.length; li++) {
|
|
920
|
+
const slot = list[li], d0 = bs[0] * (dh[slot] - dv[slot]), d1 = bs[1] * (dh[slotsCount + slot] - dv[slotsCount + slot]), d2 = bs[2] * (dh[2 * slotsCount + slot] - dv[2 * slotsCount + slot]);
|
|
921
|
+
const v = siteVar[slot], a = hs[0] * (m00 * d0 + m01 * d1 + m02 * d2), b = hs[1] * (m10 * d0 + m11 * d1 + m12 * d2), c = hs[2] * (m20 * d0 + m21 * d1 + m22 * d2);
|
|
922
|
+
x0 += a * a * v;
|
|
923
|
+
x1 += b * b * v;
|
|
924
|
+
x2 += c * c * v;
|
|
925
|
+
}
|
|
926
|
+
r0 += wR * (x0 / 4);
|
|
927
|
+
r1 += wR * (x1 / 4);
|
|
928
|
+
r2 += wR * (x2 / 4);
|
|
929
|
+
if (rt) {
|
|
930
|
+
rt[ro + 30] = x0 / 4;
|
|
931
|
+
rt[ro + 31] = x1 / 4;
|
|
932
|
+
rt[ro + 32] = x2 / 4;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
// AHD at low signal: its homogeneity choice reacts to the noise, which the linear candidates cannot express. A sample
|
|
936
|
+
// whose pixel noise exceeds the threshold adds the correction of a flat neighbourhood with its channel means, from the
|
|
937
|
+
// shared grid of states (flatCorrection): there AHD and the two linear candidates run on the same drawn neighbourhoods
|
|
938
|
+
// and the difference of the rendered results is averaged, a control variate whose expectation is the AHD bias
|
|
939
|
+
// (docs/DISPLAY_VIEW.md).
|
|
940
|
+
if (ahdCorrection && pixelVariance > ahdCorrection.thresholdVariance) {
|
|
941
|
+
const fix = flatCorrection(cmAverage[0], cmAverage[1], cmAverage[2]);
|
|
942
|
+
s0 += wE * fix[0];
|
|
943
|
+
s1 += wE * fix[1];
|
|
944
|
+
s2 += wE * fix[2];
|
|
945
|
+
corrected = true;
|
|
946
|
+
if (rt) {
|
|
947
|
+
rt[ro + 33] = 1;
|
|
948
|
+
rt[ro + 34] = fix[0];
|
|
949
|
+
rt[ro + 35] = fix[1];
|
|
950
|
+
rt[ro + 36] = fix[2];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
if (rt && memoKey >= 0) {
|
|
954
|
+
sampleMemo.set(memoKey, rt.slice());
|
|
1476
955
|
if (pk >= 0)
|
|
1477
|
-
|
|
1478
|
-
pixelVariance = 0;
|
|
956
|
+
records.set(rt, pk * MEMO_SIZE);
|
|
1479
957
|
}
|
|
1480
|
-
|
|
958
|
+
if (pk >= 0)
|
|
959
|
+
stamps[pk] = stamp;
|
|
960
|
+
pixelVariance = 0;
|
|
961
|
+
}
|
|
962
|
+
sampleCount += sampleTotal;
|
|
1481
963
|
if (corrected)
|
|
1482
964
|
correctedPixels++;
|
|
1483
|
-
|
|
965
|
+
// Stratified: means over Q samples. Exact: the weights already sum to 1 (Σ a_j = 1) and the residual carries Σ a_j²·r_j.
|
|
966
|
+
const Qd = exactView ? 1 : Q, mean0 = s0 / Qd, mean1 = s1 / Qd, mean2 = s2 / Qd, pixels = B[3];
|
|
1484
967
|
// The real pixels' output rounding (Δ²/12 in the encoded domain), mapped to linear light at the mean.
|
|
1485
968
|
const q0 = quantizationVariance(mean0, step), q1 = quantizationVariance(mean1, step), q2 = quantizationVariance(mean2, step);
|
|
1486
969
|
// Covariance in linear light: the linear part plus the independent per-pixel residuals and rounding.
|
|
1487
|
-
const v00 = c00 / Q + pixels * (r0 / Q + q0), v01 = c01 /
|
|
970
|
+
const v00 = exactView ? c00 + r0 + pixels * q0 : c00 / Q + pixels * (r0 / Q + q0), v01 = c01 / Qd, v02 = c02 / Qd, v11 = exactView ? c11 + r1 + pixels * q1 : c11 / Q + pixels * (r1 / Q + q1), v12 = c12 / Qd, v22 = exactView ? c22 + r2 + pixels * q2 : c22 / Q + pixels * (r2 / Q + q2);
|
|
1488
971
|
const noise = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_NOISE_STREAM_ID));
|
|
1489
972
|
const z0 = samplePortableStandardNormal(noise), z1 = samplePortableStandardNormal(noise), z2 = samplePortableStandardNormal(noise);
|
|
1490
973
|
// Cholesky factor of the 3 × 3 covariance (positive semidefinite; tiny negative pivots are treated as 0).
|
|
@@ -1504,14 +987,16 @@ export async function renderDisplayView(setup) {
|
|
|
1504
987
|
noiseStd[o] = Math.sqrt(Math.max(0, v00));
|
|
1505
988
|
noiseStd[o + 1] = Math.sqrt(Math.max(0, v11));
|
|
1506
989
|
noiseStd[o + 2] = Math.sqrt(Math.max(0, v22));
|
|
1507
|
-
|
|
990
|
+
// The clock is read every 16 display pixels and at the end of each row (yield timing never changes the output).
|
|
991
|
+
if (((rx & 15) === 15 || rx === region.width - 1) && Date.now() - lastYield >= 8) {
|
|
1508
992
|
await setup.yieldControl();
|
|
1509
993
|
setup.checkAbort();
|
|
1510
994
|
lastYield = Date.now();
|
|
1511
995
|
}
|
|
1512
996
|
}
|
|
1513
997
|
}
|
|
1514
|
-
return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: detail, uniformPixelCount: uniform, ahdCorrectedPixelCount: correctedPixels
|
|
998
|
+
return { samples, expected, noiseStd, sampleCount, siteCount, detailPixelCount: detail, uniformPixelCount: uniform, ahdCorrectedPixelCount: correctedPixels,
|
|
999
|
+
estimation: exactView ? "exact-area-mean" : "stratified-monte-carlo" };
|
|
1515
1000
|
}
|
|
1516
1001
|
/**
|
|
1517
1002
|
* The centre c of a normal of variance v whose clip to [0, 1] has mean `target` (bisection; c = target when the noise does
|