@photivra/engine 2.6.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 +177 -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/chart-radiance-batch.js +3 -2
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +2 -0
- package/dist/facade/chart-scenes.js +4 -3
- package/dist/facade/chart-scenes.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 -78
- package/dist/facade/display-view.js +322 -755
- 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 +44 -5
- package/dist/facade/scene-radiance.js +107 -30
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +14 -6
- package/dist/facade/simulate-capture.js +84 -23
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +24 -0
- package/dist/facade/simulate-display-view.js +117 -94
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/docs/API_REFERENCE.md +7 -1
- package/docs/QUICK_START.md +49 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +32 -2
- package/docs/api/facade-scene-radiance.md +34 -1
- package/docs/api/facade-simulate-capture.md +1 -1
- package/docs/api/facade-simulate-display-view.md +39 -0
- 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,528 +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
|
-
/** A site not in the window's site cache: from the ring, a filled window without the ring, the lazy window caches, or uncached. */
|
|
394
|
-
function siteMiss(x, y, i, j, cached) {
|
|
395
|
-
if (ringOn) {
|
|
396
|
-
if (!cached) {
|
|
397
|
-
outsideRead(i, j);
|
|
398
|
-
return ringRead(x, y, i, j);
|
|
399
|
-
}
|
|
400
|
-
const v = ringRead(x, y, i, j);
|
|
401
|
-
siteCache[j * sw + i] = v;
|
|
402
|
-
windowMaterial[j * sw + i] = lastMaterial;
|
|
403
|
-
return v;
|
|
404
|
-
}
|
|
405
|
-
if (filled && !cached)
|
|
406
|
-
outsideRead(i, j);
|
|
407
|
-
const v = filled ? filledSite(x, y, i, j) : input.kernels ? kernelSite(x, y) : directSite(x, y);
|
|
408
|
-
if (cached)
|
|
409
|
-
siteCache[j * sw + i] = v;
|
|
410
|
-
return v;
|
|
411
|
-
}
|
|
412
|
-
/**
|
|
413
|
-
* Lattice points of the closing window that kernelSite's lazy cache would have looked up: per group, the window cache points
|
|
414
|
-
* covered by the rectangles of the computed sites (the site cache's filled entries and the outside sites), a box dilation.
|
|
415
|
-
*/
|
|
416
|
-
function windowLookups() {
|
|
417
|
-
let total = 0;
|
|
418
|
-
for (const group of groups) {
|
|
419
|
-
const cw = group.cw, ch = group.ch, cpw = group.cpw, cph = group.cph, ew = cpw + cw - 1, eh = cph + ch - 1;
|
|
420
|
-
// Top-left marks in extended coordinates (window coordinates + (cw − 1, ch − 1)), and their bounding box.
|
|
421
|
-
if (marks.length < ew * eh) {
|
|
422
|
-
marks = new Uint8Array(ew * eh);
|
|
423
|
-
}
|
|
424
|
-
let x0 = ew, x1 = -1, y0 = eh, y1 = -1;
|
|
425
|
-
const mark = (ei, ej) => {
|
|
426
|
-
if (ei < 0 || ej < 0 || ei >= ew || ej >= eh)
|
|
427
|
-
return;
|
|
428
|
-
marks[ej * ew + ei] = 1;
|
|
429
|
-
if (ei < x0)
|
|
430
|
-
x0 = ei;
|
|
431
|
-
if (ei > x1)
|
|
432
|
-
x1 = ei;
|
|
433
|
-
if (ej < y0)
|
|
434
|
-
y0 = ej;
|
|
435
|
-
if (ej > y1)
|
|
436
|
-
y1 = ej;
|
|
437
|
-
};
|
|
438
|
-
for (let j = 0; j < sh; j++)
|
|
439
|
-
for (let i = 0; i < sw; i++)
|
|
440
|
-
if (!Number.isNaN(siteCache[j * sw + i]))
|
|
441
|
-
mark(i + cw - 1, j + ch - 1);
|
|
442
|
-
for (let k = 0; k < outsideSites.length; k += 2)
|
|
443
|
-
mark(outsideSites[k] + cw - 1, outsideSites[k + 1] + ch - 1);
|
|
444
|
-
if (x1 < 0)
|
|
445
|
-
continue;
|
|
446
|
-
// Cache point (i, j) is covered when a mark lies in extended columns [i, i + cw) and rows [j, j + ch): a horizontal
|
|
447
|
-
// then a vertical running count over the marks' box (outside it nothing is covered).
|
|
448
|
-
const i0 = Math.max(0, x0 - cw + 1), i1 = Math.min(cpw - 1, x1), cwid = i1 - i0 + 1;
|
|
449
|
-
if (cover.length < cwid * (y1 - y0 + 1))
|
|
450
|
-
cover = new Uint8Array(cwid * (y1 - y0 + 1));
|
|
451
|
-
for (let r = y0; r <= y1; r++) {
|
|
452
|
-
let run = 0;
|
|
453
|
-
for (let e = i0; e <= i1 + cw - 1; e++) {
|
|
454
|
-
run += marks[r * ew + e];
|
|
455
|
-
if (e - cw >= i0)
|
|
456
|
-
run -= marks[r * ew + e - cw];
|
|
457
|
-
if (e >= i0 + cw - 1)
|
|
458
|
-
cover[(r - y0) * cwid + e - cw + 1 - i0] = run > 0 ? 1 : 0;
|
|
459
|
-
}
|
|
460
|
-
marks.fill(0, r * ew + x0, r * ew + x1 + 1);
|
|
461
|
-
}
|
|
462
|
-
for (let i = 0; i < cwid; i++) {
|
|
463
|
-
let run = 0;
|
|
464
|
-
for (let r = y0; r <= y1 + ch - 1; r++) {
|
|
465
|
-
if (r <= y1)
|
|
466
|
-
run += cover[(r - y0) * cwid + i];
|
|
467
|
-
if (r - ch >= y0 && r - ch <= y1)
|
|
468
|
-
run -= cover[(r - ch - y0) * cwid + i];
|
|
469
|
-
const j = r - ch + 1;
|
|
470
|
-
if (j >= 0 && j < cph && run > 0)
|
|
471
|
-
total++;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return total;
|
|
476
|
-
}
|
|
477
|
-
let sx0 = 0, sy0 = 0, sw = 0, sh = 0, siteCache = new Float64Array(0);
|
|
478
|
-
const pilotNode = input.wavelengthsNm[Math.floor(nodes / 2)];
|
|
479
|
-
return {
|
|
480
|
-
get lookups() { return lookups + (filled ? windowLookups() : 0); },
|
|
481
|
-
get uniformMaterial() { return lastMaterial; },
|
|
482
|
-
get tracksMaterial() { return ringOn; },
|
|
483
|
-
window(x0, y0, x1, y1) {
|
|
484
|
-
if (filled)
|
|
485
|
-
lookups += windowLookups();
|
|
486
|
-
filled = false;
|
|
487
|
-
ringOn = false;
|
|
488
|
-
outsideSites = [];
|
|
489
|
-
sx0 = x0;
|
|
490
|
-
sy0 = y0;
|
|
491
|
-
sw = Math.max(0, x1 - x0);
|
|
492
|
-
sh = Math.max(0, y1 - y0);
|
|
493
|
-
if (sw * sh > CACHE_LIMIT) {
|
|
494
|
-
sw = 0;
|
|
495
|
-
sh = 0;
|
|
496
|
-
}
|
|
497
|
-
if (siteCache.length < sw * sh)
|
|
498
|
-
siteCache = new Float64Array(sw * sh);
|
|
499
|
-
siteCache.fill(NaN, 0, sw * sh);
|
|
500
|
-
if (!input.kernels)
|
|
501
|
-
return;
|
|
502
|
-
for (const group of groups) {
|
|
503
|
-
group.cx0 = x0 + group.minIx - R;
|
|
504
|
-
group.cy0 = y0 + group.minIy - R;
|
|
505
|
-
group.cpw = sw === 0 ? 0 : sw + group.cw - 1;
|
|
506
|
-
group.cph = sh === 0 ? 0 : sh + group.ch - 1;
|
|
507
|
-
if (group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes) > CACHE_LIMIT) {
|
|
508
|
-
group.cpw = 0;
|
|
509
|
-
group.cph = 0;
|
|
510
|
-
}
|
|
511
|
-
}
|
|
512
|
-
// Chart materials whose every group caches the whole window: the filled path (bands). Otherwise the lazy window caches.
|
|
513
|
-
if (scene.kind === "materials" && sw > 0 && groups.every((group) => group.cpw > 0)) {
|
|
514
|
-
groups.forEach((group, g) => coverBand(g, group.cx0, group.cx0 + group.cpw, group.cy0, group.cph));
|
|
515
|
-
filled = true;
|
|
516
|
-
if (windowMaterial.length < sw * sh)
|
|
517
|
-
windowMaterial = new Int32Array(sw * sh);
|
|
518
|
-
// A ring of at least twice the window's rows keeps the rows of the previous display row's windows.
|
|
519
|
-
if (ringRows < 2 * sh) {
|
|
520
|
-
let rows = 1;
|
|
521
|
-
while (rows < 2 * sh)
|
|
522
|
-
rows *= 2;
|
|
523
|
-
if (rows * W <= CACHE_LIMIT) {
|
|
524
|
-
ringRows = rows;
|
|
525
|
-
ring = new Float64Array(rows * W).fill(NaN);
|
|
526
|
-
ringMaterial = new Int32Array(rows * W);
|
|
527
|
-
ringRowId = new Int32Array(rows).fill(-1);
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
ringOn = ringRows >= 2 * sh;
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
for (const group of groups) {
|
|
534
|
-
const n = group.cpw * group.cph * (scene.kind === "materials" ? 1 : nodes);
|
|
535
|
-
if (scene.kind === "materials") {
|
|
536
|
-
if (group.cache.length < n || !(group.cache instanceof Int32Array))
|
|
537
|
-
group.cache = new Int32Array(n);
|
|
538
|
-
group.cache.fill(-1, 0, n);
|
|
539
|
-
}
|
|
540
|
-
else {
|
|
541
|
-
if (group.cache.length < n || !(group.cache instanceof Float64Array))
|
|
542
|
-
group.cache = new Float64Array(n);
|
|
543
|
-
group.cache.fill(NaN, 0, n);
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
},
|
|
547
|
-
site(x, y) {
|
|
548
|
-
// The window's site cache first (with the ring: the sites read in this window, which its lookup count needs too).
|
|
549
|
-
const i = x - sx0, j = y - sy0, cached = i >= 0 && j >= 0 && i < sw && j < sh;
|
|
550
|
-
if (cached) {
|
|
551
|
-
const w = j * sw + i, v = siteCache[w];
|
|
552
|
-
if (v === v) {
|
|
553
|
-
if (ringOn)
|
|
554
|
-
lastMaterial = windowMaterial[w];
|
|
555
|
-
return v;
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
return siteMiss(x, y, i, j, cached);
|
|
559
|
-
},
|
|
560
|
-
pilot(u, v) {
|
|
561
|
-
lookups++;
|
|
562
|
-
return scene.kind === "materials" ? scene.materialAt(u, v) : scene.radianceAt(u, v, pilotNode);
|
|
563
|
-
}
|
|
564
|
-
};
|
|
565
|
-
}
|
|
43
|
+
export { createDisplayViewExpectedSignal } from "./expected-signal.js";
|
|
566
44
|
// ---------------------------------------------------------------------------------------------------------------
|
|
567
45
|
// Normal-distribution helpers.
|
|
568
46
|
/** Standard normal density. */
|
|
@@ -841,6 +319,15 @@ function correctionStates(key) {
|
|
|
841
319
|
* AHD choice terms; 33 a 1 when the AHD correction applies and [34, 37) its values.
|
|
842
320
|
*/
|
|
843
321
|
const MEMO_SIZE = 37;
|
|
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. */
|
|
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;
|
|
844
331
|
/** Pilot points per axis: about one per two sites of the footprint plus its reach, 4 to 16. */
|
|
845
332
|
function pilotCount(span) { return Math.max(4, Math.min(16, Math.ceil(span / 2))); }
|
|
846
333
|
/** Renders the display view (docs/DISPLAY_VIEW.md). */
|
|
@@ -848,6 +335,21 @@ export async function renderDisplayView(setup) {
|
|
|
848
335
|
const { frameWidth, frameHeight, margin, nativeWidth, nativeHeight, viewWidth, viewHeight, region, seed, reachSites } = setup;
|
|
849
336
|
const sx = frameWidth / viewWidth, sy = frameHeight / viewHeight, area = sx * sy, g = 2 / frameWidth;
|
|
850
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);
|
|
851
353
|
const maximum = setup.bitDepth === 8 ? 255 : 65535, step = 1 / maximum;
|
|
852
354
|
const pixelCount = region.width * region.height;
|
|
853
355
|
const samples = setup.bitDepth === 8 ? new Uint8Array(pixelCount * 3) : new Uint16Array(pixelCount * 3);
|
|
@@ -1062,7 +564,14 @@ export async function renderDisplayView(setup) {
|
|
|
1062
564
|
}
|
|
1063
565
|
let siteCount = 0, sampleCount = 0, detail = 0, uniform = 0, lastYield = Date.now();
|
|
1064
566
|
/** Recorded terms of samples whose sites all read one chart material, by material·4 + CFA phase (layout: MEMO_SIZE). */
|
|
1065
|
-
const sampleMemo = new Map();
|
|
567
|
+
const sampleMemo = new Map(), recordScratch = new Float64Array(MEMO_SIZE);
|
|
568
|
+
// Per-real-pixel memo (#392): a sample's recorded terms depend only on its real pixel (its sites' expected electrons and its
|
|
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
|
|
570
|
+
// replays them (the same values added to the same sums in the same order, as the one-material memo does). Slots cover the
|
|
571
|
+
// display pixel's footprint and are valid while their stamp equals the display pixel's generation. A replayed sample still
|
|
572
|
+
// counts its sites; it is used only where every site lies in the window's site cache, where reading it again would have
|
|
573
|
+
// cost no work and no scene lookup.
|
|
574
|
+
let pixelStamp = new Int32Array(0), pixelRecords = new Float64Array(0), pixelGeneration = 0;
|
|
1066
575
|
// Pilot: the scene at a deterministic grid over each display pixel's footprint widened by the reach; a pixel is uniform when
|
|
1067
576
|
// every point returns the same value. Pixels next to a detailed pixel count as detailed too, so an edge that only clips a
|
|
1068
577
|
// footprint's corner (too thin for the grid) still gets the full samples. The grid is fixed, so the sample count is not
|
|
@@ -1087,7 +596,8 @@ export async function renderDisplayView(setup) {
|
|
|
1087
596
|
const ringWidth = region.width + 2, flagRows = [];
|
|
1088
597
|
const pilotRow = (vy) => {
|
|
1089
598
|
const row = new Uint8Array(ringWidth);
|
|
1090
|
-
|
|
599
|
+
// The exact area mean needs no pilot: every pixel evaluates its whole footprint.
|
|
600
|
+
if (exactView || vy < 0 || vy >= viewHeight)
|
|
1091
601
|
return row.fill(1);
|
|
1092
602
|
for (let i = 0; i < ringWidth; i++) {
|
|
1093
603
|
const vx = region.x - 1 + i;
|
|
@@ -1097,7 +607,7 @@ export async function renderDisplayView(setup) {
|
|
|
1097
607
|
};
|
|
1098
608
|
flagRows.push(pilotRow(region.y - 1), pilotRow(region.y));
|
|
1099
609
|
for (let ry = 0; ry < region.height; ry++) {
|
|
1100
|
-
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;
|
|
1101
611
|
flagRows.push(pilotRow(vy + 1));
|
|
1102
612
|
if (flagRows.length > 3)
|
|
1103
613
|
flagRows.shift();
|
|
@@ -1112,22 +622,62 @@ export async function renderDisplayView(setup) {
|
|
|
1112
622
|
break;
|
|
1113
623
|
}
|
|
1114
624
|
const K = same ? uniformK : fullK, Q = K * K;
|
|
1115
|
-
|
|
625
|
+
// On the exact path every pixel counts as detailed (its whole footprint is evaluated).
|
|
626
|
+
if (exactView)
|
|
627
|
+
detail++;
|
|
628
|
+
else if (same)
|
|
1116
629
|
uniform++;
|
|
1117
630
|
else
|
|
1118
631
|
detail++;
|
|
632
|
+
const cols = exactView ? columnOverlaps[rx] : undefined, nx = cols ? cols.w.length : 0, sampleTotal = exactView ? nx * rowOverlaps.w.length : Q;
|
|
1119
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);
|
|
1120
634
|
const jitter = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_JITTER_STREAM_ID)), tracking = setup.signal.tracksMaterial;
|
|
635
|
+
const mx0 = Math.floor(vx * sx) + margin, my0 = Math.floor(vy * sy) + margin;
|
|
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;
|
|
637
|
+
const pixelMemo = !exactView && setup.signal.cachesSites && mw * mh <= PIXEL_MEMO_LIMIT;
|
|
638
|
+
if (pixelMemo) {
|
|
639
|
+
if (pixelStamp.length < mw * mh) {
|
|
640
|
+
pixelStamp = new Int32Array(mw * mh);
|
|
641
|
+
pixelRecords = new Float64Array(mw * mh * MEMO_SIZE);
|
|
642
|
+
pixelGeneration = 0;
|
|
643
|
+
}
|
|
644
|
+
pixelGeneration++;
|
|
645
|
+
}
|
|
1121
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;
|
|
1122
|
-
for (let
|
|
1123
|
-
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;
|
|
1124
660
|
const fx = (vx + (a + jitter.next()) / K) * sx, fy = (vy + (b + jitter.next()) / K) * sy;
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
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)
|
|
1131
681
|
for (let li = 0; li < list.length; li++) {
|
|
1132
682
|
const slot = list[li];
|
|
1133
683
|
siteCount++;
|
|
@@ -1162,247 +712,262 @@ export async function renderDisplayView(setup) {
|
|
|
1162
712
|
siteVar[slot] = clip[1];
|
|
1163
713
|
}
|
|
1164
714
|
}
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
c00 += wgt * a0 * a0;
|
|
1176
|
-
c01 += wgt * a0 * a1;
|
|
1177
|
-
c02 += wgt * a0 * a2;
|
|
1178
|
-
c11 += wgt * a1 * a1;
|
|
1179
|
-
c12 += wgt * a1 * a2;
|
|
1180
|
-
c22 += wgt * a2 * a2;
|
|
1181
|
-
}
|
|
1182
|
-
r0 += memo[21 + d * 3];
|
|
1183
|
-
r1 += memo[22 + d * 3];
|
|
1184
|
-
r2 += memo[23 + d * 3];
|
|
1185
|
-
}
|
|
1186
|
-
s0 += memo[27];
|
|
1187
|
-
s1 += memo[28];
|
|
1188
|
-
s2 += memo[29];
|
|
1189
|
-
if (count === 2) {
|
|
1190
|
-
r0 += memo[30];
|
|
1191
|
-
r1 += memo[31];
|
|
1192
|
-
r2 += memo[32];
|
|
1193
|
-
}
|
|
1194
|
-
if (memo[33] === 1) {
|
|
1195
|
-
s0 += memo[34];
|
|
1196
|
-
s1 += memo[35];
|
|
1197
|
-
s2 += memo[36];
|
|
1198
|
-
corrected = true;
|
|
1199
|
-
}
|
|
1200
|
-
continue;
|
|
1201
|
-
}
|
|
1202
|
-
const record = memoKey >= 0 ? new Float64Array(MEMO_SIZE) : undefined;
|
|
1203
|
-
if (tracking)
|
|
1204
|
-
for (let li = 0; li < list.length; li++) {
|
|
1205
|
-
const slot = list[li];
|
|
1206
|
-
siteStatistics(slot, siteChannel[slot], siteElectrons[slot]);
|
|
1207
|
-
}
|
|
1208
|
-
// Site variance of each channel through the bilinear kernels (for B_c), then the demosaic's own pixel statistics: the
|
|
1209
|
-
// mean and covariance of its camera channels, averaged over its candidates (AHD picks one per pixel).
|
|
1210
|
-
const slots = phaseSlots[phase], weights = phaseWeights[phase], w2 = phaseW2[phase];
|
|
1211
|
-
for (let c = 0; c < 3; c++) {
|
|
1212
|
-
const sl = slots[c], wt = weights[c];
|
|
1213
|
-
let variance = 0;
|
|
1214
|
-
for (let t = 0; t < sl.length; t++)
|
|
1215
|
-
variance += wt[t] * wt[t] * siteVar[sl[t]];
|
|
1216
|
-
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;
|
|
1217
725
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
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) {
|
|
1222
729
|
for (let d = 0; d < count; d++) {
|
|
1223
730
|
for (let c = 0; c < 3; c++) {
|
|
1224
|
-
const
|
|
1225
|
-
let mean = 0;
|
|
1226
|
-
for (let t = 0; t < sl.length; t++)
|
|
1227
|
-
mean += wt[t] * siteMean[sl[t]];
|
|
1228
|
-
cm[c] = mean;
|
|
1229
|
-
cmAverage[c] = (d === 0 ? 0 : cmAverage[c]) + mean * share;
|
|
1230
|
-
}
|
|
1231
|
-
// Covariance of channels i, j: Σ over the sites of w_i·w_j·Var(site), with the kernels as dense weights over the window.
|
|
1232
|
-
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
1233
|
-
let v00 = 0, v01 = 0, v02 = 0, v11 = 0, v12 = 0, v22 = 0;
|
|
1234
|
-
for (let li = 0; li < list.length; li++) {
|
|
1235
|
-
const slot = list[li], v = siteVar[slot], w0 = dense[slot], w1 = dense[slotsCount + slot], w2 = dense[2 * slotsCount + slot];
|
|
1236
|
-
v00 += w0 * w0 * v;
|
|
1237
|
-
v01 += w0 * w1 * v;
|
|
1238
|
-
v02 += w0 * w2 * v;
|
|
1239
|
-
v11 += w1 * w1 * v;
|
|
1240
|
-
v12 += w1 * w2 * v;
|
|
1241
|
-
v22 += w2 * w2 * v;
|
|
1242
|
-
}
|
|
1243
|
-
C[0] = v00;
|
|
1244
|
-
C[1] = v01;
|
|
1245
|
-
C[2] = v02;
|
|
1246
|
-
C[3] = v01;
|
|
1247
|
-
C[4] = v11;
|
|
1248
|
-
C[5] = v12;
|
|
1249
|
-
C[6] = v02;
|
|
1250
|
-
C[7] = v12;
|
|
1251
|
-
C[8] = v22;
|
|
1252
|
-
// White balance and the highlight clip at the smallest gain (per channel; covariances scaled by the expected slopes).
|
|
1253
|
-
for (let c = 0; c < 3; c++) {
|
|
1254
|
-
const mean = cm[c], variance = C[c * 4];
|
|
1255
|
-
cv[c] = variance;
|
|
1256
|
-
const gc = c === 0 ? g0 : c === 1 ? g1 : g2, um = gc * mean, uv = gc * gc * variance, gap = L - um;
|
|
1257
|
-
if (gap > 0 && gap * gap > reach2 * uv) {
|
|
1258
|
-
bm[c] = um;
|
|
1259
|
-
bv[c] = uv;
|
|
1260
|
-
bs[c] = gc;
|
|
1261
|
-
}
|
|
1262
|
-
else {
|
|
1263
|
-
upperClip(um, uv, L, clip);
|
|
1264
|
-
bm[c] = clip[0];
|
|
1265
|
-
bv[c] = clip[1];
|
|
1266
|
-
bs[c] = gc * clip[2];
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
// Colour matrix, then the rendering of each component (tone curve, clip to [0, 1]).
|
|
1270
|
-
for (let comp = 0; comp < 3; comp++) {
|
|
1271
|
-
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;
|
|
1272
|
-
const mean = r0m * bm[0] + r1m * bm[1] + r2m * bm[2];
|
|
1273
|
-
const variance = r0m * r0m * bv[0] + r1m * r1m * bv[1] + r2m * r2m * bv[2] +
|
|
1274
|
-
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]);
|
|
1275
|
-
if (!reinhard && mean > 0 && mean < 1 && mean * mean > reach2 * variance && (1 - mean) * (1 - mean) > reach2 * variance) {
|
|
1276
|
-
hm[comp] = mean;
|
|
1277
|
-
hv[comp] = variance;
|
|
1278
|
-
hs[comp] = 1;
|
|
1279
|
-
}
|
|
1280
|
-
else if (reinhard) {
|
|
1281
|
-
reinhardMoments(mean, variance, clip);
|
|
1282
|
-
hm[comp] = clip[0];
|
|
1283
|
-
hv[comp] = clip[1];
|
|
1284
|
-
hs[comp] = clip[2];
|
|
1285
|
-
}
|
|
1286
|
-
else {
|
|
1287
|
-
// The component is a linear combination of the sites: κ₃ = Σ (Σ_c M·Γ_c·w_c,site)³·κ₃(site).
|
|
1288
|
-
const dense = candidateDense[phase][d], list = candidateList[phase][d];
|
|
1289
|
-
let third = 0;
|
|
1290
|
-
for (let li = 0; li < list.length; li++) {
|
|
1291
|
-
const slot = list[li], coefficient = r0m * bs[0] * dense[slot] + r1m * bs[1] * dense[slotsCount + slot] + r2m * bs[2] * dense[2 * slotsCount + slot];
|
|
1292
|
-
third += coefficient * coefficient * coefficient * siteThird[slot];
|
|
1293
|
-
}
|
|
1294
|
-
unitClip(mean, variance, clip, variance > 0 ? third / (variance * Math.sqrt(variance)) : 0);
|
|
1295
|
-
hm[comp] = clip[0];
|
|
1296
|
-
hv[comp] = clip[1];
|
|
1297
|
-
hs[comp] = clip[2];
|
|
1298
|
-
}
|
|
1299
|
-
}
|
|
1300
|
-
e0 += share * hm[0];
|
|
1301
|
-
e1 += share * hm[1];
|
|
1302
|
-
e2 += share * hm[2];
|
|
1303
|
-
// Linear (Stein) part: a_c = J ∘ M[:, c]·Γ_c, accumulated with B_c·σ_c² (site variance of channel c); the pixel's own
|
|
1304
|
-
// linear variance (aᵀ·C·a, full channel covariance) for the residual.
|
|
1305
|
-
let l0 = 0, l1 = 0, l2 = 0;
|
|
1306
|
-
for (let c = 0; c < 3; c++) {
|
|
1307
|
-
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];
|
|
1308
|
-
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];
|
|
1309
732
|
c00 += wgt * a0 * a0;
|
|
1310
733
|
c01 += wgt * a0 * a1;
|
|
1311
734
|
c02 += wgt * a0 * a2;
|
|
1312
735
|
c11 += wgt * a1 * a1;
|
|
1313
736
|
c12 += wgt * a1 * a2;
|
|
1314
737
|
c22 += wgt * a2 * a2;
|
|
1315
|
-
an[c] = a0;
|
|
1316
|
-
an[3 + c] = a1;
|
|
1317
|
-
an[6 + c] = a2;
|
|
1318
|
-
if (record) {
|
|
1319
|
-
const o = 3 + (d * 3 + c) * 3;
|
|
1320
|
-
record[o] = a0;
|
|
1321
|
-
record[o + 1] = a1;
|
|
1322
|
-
record[o + 2] = a2;
|
|
1323
|
-
}
|
|
1324
738
|
}
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
l0 += an[i] * an[j] * cij;
|
|
1329
|
-
l1 += an[3 + i] * an[3 + j] * cij;
|
|
1330
|
-
l2 += an[6 + i] * an[6 + j] * cij;
|
|
1331
|
-
}
|
|
1332
|
-
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);
|
|
1333
|
-
r0 += t0;
|
|
1334
|
-
r1 += t1;
|
|
1335
|
-
r2 += t2;
|
|
1336
|
-
if (record) {
|
|
1337
|
-
record[21 + d * 3] = t0;
|
|
1338
|
-
record[22 + d * 3] = t1;
|
|
1339
|
-
record[23 + d * 3] = t2;
|
|
1340
|
-
}
|
|
1341
|
-
pixelVariance = Math.max(pixelVariance, hv[0], hv[1], hv[2]);
|
|
1342
|
-
}
|
|
1343
|
-
s0 += e0;
|
|
1344
|
-
s1 += e1;
|
|
1345
|
-
s2 += e2;
|
|
1346
|
-
if (record) {
|
|
1347
|
-
record[0] = csv[0];
|
|
1348
|
-
record[1] = csv[1];
|
|
1349
|
-
record[2] = csv[2];
|
|
1350
|
-
record[27] = e0;
|
|
1351
|
-
record[28] = e1;
|
|
1352
|
-
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];
|
|
1353
742
|
}
|
|
743
|
+
s0 += wE * memo[mo + 27];
|
|
744
|
+
s1 += wE * memo[mo + 28];
|
|
745
|
+
s2 += wE * memo[mo + 29];
|
|
1354
746
|
if (count === 2) {
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
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;
|
|
820
|
+
}
|
|
821
|
+
else {
|
|
822
|
+
upperClip(um, uv, L, clip);
|
|
823
|
+
bm[c] = clip[0];
|
|
824
|
+
bv[c] = clip[1];
|
|
825
|
+
bs[c] = gc * clip[2];
|
|
826
|
+
}
|
|
827
|
+
}
|
|
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];
|
|
1366
857
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
858
|
+
}
|
|
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;
|
|
1374
882
|
}
|
|
1375
883
|
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
const fix = flatCorrection(cmAverage[0], cmAverage[1], cmAverage[2]);
|
|
1383
|
-
s0 += fix[0];
|
|
1384
|
-
s1 += fix[1];
|
|
1385
|
-
s2 += fix[2];
|
|
1386
|
-
corrected = true;
|
|
1387
|
-
if (record) {
|
|
1388
|
-
record[33] = 1;
|
|
1389
|
-
record[34] = fix[0];
|
|
1390
|
-
record[35] = fix[1];
|
|
1391
|
-
record[36] = fix[2];
|
|
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;
|
|
1392
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;
|
|
1393
899
|
}
|
|
1394
|
-
|
|
1395
|
-
sampleMemo.set(memoKey, record);
|
|
1396
|
-
pixelVariance = 0;
|
|
900
|
+
pixelVariance = Math.max(pixelVariance, hv[0], hv[1], hv[2]);
|
|
1397
901
|
}
|
|
1398
|
-
|
|
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());
|
|
955
|
+
if (pk >= 0)
|
|
956
|
+
records.set(rt, pk * MEMO_SIZE);
|
|
957
|
+
}
|
|
958
|
+
if (pk >= 0)
|
|
959
|
+
stamps[pk] = stamp;
|
|
960
|
+
pixelVariance = 0;
|
|
961
|
+
}
|
|
962
|
+
sampleCount += sampleTotal;
|
|
1399
963
|
if (corrected)
|
|
1400
964
|
correctedPixels++;
|
|
1401
|
-
|
|
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];
|
|
1402
967
|
// The real pixels' output rounding (Δ²/12 in the encoded domain), mapped to linear light at the mean.
|
|
1403
968
|
const q0 = quantizationVariance(mean0, step), q1 = quantizationVariance(mean1, step), q2 = quantizationVariance(mean2, step);
|
|
1404
969
|
// Covariance in linear light: the linear part plus the independent per-pixel residuals and rounding.
|
|
1405
|
-
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);
|
|
1406
971
|
const noise = createDeterministicUniformStream(deriveNativeSiteStreamSeedUint32(seed, index, DISPLAY_VIEW_NOISE_STREAM_ID));
|
|
1407
972
|
const z0 = samplePortableStandardNormal(noise), z1 = samplePortableStandardNormal(noise), z2 = samplePortableStandardNormal(noise);
|
|
1408
973
|
// Cholesky factor of the 3 × 3 covariance (positive semidefinite; tiny negative pivots are treated as 0).
|
|
@@ -1422,14 +987,16 @@ export async function renderDisplayView(setup) {
|
|
|
1422
987
|
noiseStd[o] = Math.sqrt(Math.max(0, v00));
|
|
1423
988
|
noiseStd[o + 1] = Math.sqrt(Math.max(0, v11));
|
|
1424
989
|
noiseStd[o + 2] = Math.sqrt(Math.max(0, v22));
|
|
1425
|
-
|
|
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) {
|
|
1426
992
|
await setup.yieldControl();
|
|
1427
993
|
setup.checkAbort();
|
|
1428
994
|
lastYield = Date.now();
|
|
1429
995
|
}
|
|
1430
996
|
}
|
|
1431
997
|
}
|
|
1432
|
-
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" };
|
|
1433
1000
|
}
|
|
1434
1001
|
/**
|
|
1435
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
|