@photivra/engine 2.21.0 → 2.22.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.
Files changed (169) hide show
  1. package/CHANGELOG.md +118 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +11 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-opcodes.d.ts +96 -0
  13. package/dist/capture/dng-opcodes.js +391 -0
  14. package/dist/capture/dng-opcodes.js.map +1 -0
  15. package/dist/capture/dng-profile-maps.d.ts +99 -0
  16. package/dist/capture/dng-profile-maps.js +381 -0
  17. package/dist/capture/dng-profile-maps.js.map +1 -0
  18. package/dist/capture/dng-stage-image.d.ts +23 -0
  19. package/dist/capture/dng-stage-image.js +65 -0
  20. package/dist/capture/dng-stage-image.js.map +1 -0
  21. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  22. package/dist/capture/jpeg-xl-tiles.js +151 -0
  23. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  24. package/dist/capture/lossy-jpeg.d.ts +17 -0
  25. package/dist/capture/lossy-jpeg.js +369 -0
  26. package/dist/capture/lossy-jpeg.js.map +1 -0
  27. package/dist/capture/native-raw-development.d.ts +2 -1
  28. package/dist/capture/native-raw-development.js.map +1 -1
  29. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  30. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  31. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  32. package/dist/color/sensor-color-development.d.ts +5 -0
  33. package/dist/color/sensor-color-development.js +7 -1
  34. package/dist/color/sensor-color-development.js.map +1 -1
  35. package/dist/color/spectral-spikiness.d.ts +11 -0
  36. package/dist/color/spectral-spikiness.js +23 -0
  37. package/dist/color/spectral-spikiness.js.map +1 -0
  38. package/dist/core/execution-error.d.ts +21 -0
  39. package/dist/core/execution-error.js +12 -0
  40. package/dist/core/execution-error.js.map +1 -0
  41. package/dist/core/version.d.ts +1 -1
  42. package/dist/core/version.js +1 -1
  43. package/dist/core/version.js.map +1 -1
  44. package/dist/facade/catalogue-lens-signal.d.ts +26 -1
  45. package/dist/facade/catalogue-lens-signal.js +25 -7
  46. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  47. package/dist/facade/diffraction-provider.d.ts +1 -1
  48. package/dist/facade/diffraction-provider.js +4 -4
  49. package/dist/facade/diffraction-provider.js.map +1 -1
  50. package/dist/facade/display-view-gpu-session.js +38 -6
  51. package/dist/facade/display-view-gpu-session.js.map +1 -1
  52. package/dist/facade/display-view-gpu.d.ts +13 -5
  53. package/dist/facade/display-view-gpu.js.map +1 -1
  54. package/dist/facade/display-view-normal.d.ts +15 -0
  55. package/dist/facade/display-view-normal.js +44 -0
  56. package/dist/facade/display-view-normal.js.map +1 -0
  57. package/dist/facade/display-view-rendering.d.ts +97 -0
  58. package/dist/facade/display-view-rendering.js +598 -0
  59. package/dist/facade/display-view-rendering.js.map +1 -0
  60. package/dist/facade/display-view.d.ts +37 -65
  61. package/dist/facade/display-view.js +147 -476
  62. package/dist/facade/display-view.js.map +1 -1
  63. package/dist/facade/expected-signal-setup.js +2 -2
  64. package/dist/facade/expected-signal-setup.js.map +1 -1
  65. package/dist/facade/fast-methods.d.ts +1 -1
  66. package/dist/facade/fast-methods.js +37 -8
  67. package/dist/facade/fast-methods.js.map +1 -1
  68. package/dist/facade/generic-lens-capture.d.ts +1 -1
  69. package/dist/facade/generic-lens-capture.js +1 -1
  70. package/dist/facade/generic-lens-capture.js.map +1 -1
  71. package/dist/facade/image-plane.js +41 -10
  72. package/dist/facade/image-plane.js.map +1 -1
  73. package/dist/facade/on-demand-kits.d.ts +14 -0
  74. package/dist/facade/on-demand-kits.js +36 -0
  75. package/dist/facade/on-demand-kits.js.map +1 -0
  76. package/dist/facade/opt-in-kit.d.ts +9 -0
  77. package/dist/facade/opt-in-kit.js +11 -0
  78. package/dist/facade/opt-in-kit.js.map +1 -0
  79. package/dist/facade/quality-kit.d.ts +21 -0
  80. package/dist/facade/quality-kit.js +23 -0
  81. package/dist/facade/quality-kit.js.map +1 -0
  82. package/dist/facade/quality-paths.d.ts +22 -0
  83. package/dist/facade/quality-paths.js +44 -0
  84. package/dist/facade/quality-paths.js.map +1 -0
  85. package/dist/facade/scene-radiance.d.ts +18 -2
  86. package/dist/facade/scene-radiance.js +6 -4
  87. package/dist/facade/scene-radiance.js.map +1 -1
  88. package/dist/facade/simulate-capture.d.ts +32 -13
  89. package/dist/facade/simulate-capture.js +162 -69
  90. package/dist/facade/simulate-capture.js.map +1 -1
  91. package/dist/facade/simulate-display-view.d.ts +10 -12
  92. package/dist/facade/simulate-display-view.js +57 -48
  93. package/dist/facade/simulate-display-view.js.map +1 -1
  94. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  95. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  96. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  97. package/dist/gpu/display-view-image-material-module.d.ts +3 -2
  98. package/dist/gpu/display-view-image-material-module.js +25 -3
  99. package/dist/gpu/display-view-image-material-module.js.map +1 -1
  100. package/dist/gpu/display-view-image-quality.d.ts +43 -1
  101. package/dist/gpu/display-view-image-quality.js +256 -11
  102. package/dist/gpu/display-view-image-quality.js.map +1 -1
  103. package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
  104. package/dist/gpu/display-view-quality-gpu.js +60 -249
  105. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  106. package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
  107. package/dist/gpu/display-view-speed-gpu.js +50 -12
  108. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  109. package/dist/gpu/image-kernel-accelerator.js +3 -0
  110. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  111. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  112. package/dist/gpu/image-kernel-execution.js +2 -10
  113. package/dist/gpu/image-kernel-execution.js.map +1 -1
  114. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  115. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  116. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  117. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  118. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  119. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  120. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
  121. package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
  122. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  123. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
  124. package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
  125. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  126. package/dist/index.d.ts +11 -3
  127. package/dist/index.js +9 -1
  128. package/dist/index.js.map +1 -1
  129. package/dist/output/camera-channel-response.d.ts +50 -0
  130. package/dist/output/camera-channel-response.js +69 -0
  131. package/dist/output/camera-channel-response.js.map +1 -0
  132. package/dist/output/native-capture-sdr.js +5 -2
  133. package/dist/output/native-capture-sdr.js.map +1 -1
  134. package/dist/output/sdr-rendering.d.ts +18 -1
  135. package/dist/output/sdr-rendering.js +39 -2
  136. package/dist/output/sdr-rendering.js.map +1 -1
  137. package/dist/sensor/native-sensor-report.d.ts +10 -0
  138. package/dist/sensor/native-sensor-report.js +16 -0
  139. package/dist/sensor/native-sensor-report.js.map +1 -0
  140. package/dist/sensor/sensor-characteristics.d.ts +81 -13
  141. package/dist/sensor/sensor-characteristics.js +10 -6
  142. package/dist/sensor/sensor-characteristics.js.map +1 -1
  143. package/docs/API_REFERENCE.md +117 -4
  144. package/docs/QUICK_START.md +37 -8
  145. package/docs/USAGE.md +104 -22
  146. package/docs/api/capture-c2pa-embed.md +177 -0
  147. package/docs/api/capture-dng-import.md +86 -10
  148. package/docs/api/capture-dng-opcodes.md +93 -0
  149. package/docs/api/capture-dng-profile-maps.md +141 -0
  150. package/docs/api/capture-dng-stage-image.md +43 -0
  151. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  152. package/docs/api/capture-lossy-jpeg.md +41 -0
  153. package/docs/api/capture-native-raw-development.md +2 -1
  154. package/docs/api/core-execution-error.md +43 -0
  155. package/docs/api/core-version.md +1 -1
  156. package/docs/api/exports.json +255 -5
  157. package/docs/api/facade-display-view-gpu.md +5 -2
  158. package/docs/api/facade-display-view.md +11 -15
  159. package/docs/api/facade-fast-methods.md +38 -9
  160. package/docs/api/facade-quality-paths.md +24 -0
  161. package/docs/api/facade-scene-radiance.md +1 -1
  162. package/docs/api/facade-simulate-capture.md +24 -7
  163. package/docs/api/facade-simulate-display-view.md +10 -12
  164. package/docs/api/gpu-image-kernel-execution.md +0 -36
  165. package/docs/api/output-camera-channel-response.md +55 -0
  166. package/docs/api/output-sdr-rendering.md +6 -1
  167. package/docs/api/sensor-sensor-characteristics.md +54 -2
  168. package/llms.txt +1 -0
  169. package/package.json +6 -5
@@ -0,0 +1,598 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * #593 (2.22.0): the display view's rendering steps that couple a pixel's three components: the luminance tone curves
4
+ * (`extended-reinhard-luminance`, `naka-rushton-luminance`), `highlightMode: "reconstruct"` and (#601) the hue-preserving
5
+ * gamut handling `desaturate-to-luminance`. The view never draws a pixel's noise; it propagates the mean and covariance of the
6
+ * pixel's camera channels (normal, docs/DISPLAY_VIEW.md) through development, so a coupled step needs the moments of a function
7
+ * of three correlated normals. Two methods, both acting on each pixel's own developed values at use (no lookup tables):
8
+ *
9
+ * - **Tone curves by conditioning on luminance.** With the developed linear sRGB y ~ N(μ, Σ) and the luminance L = wᵀy (w the
10
+ * sRGB Y row), y_k given L is normal with mean μ_k + β_k(L − μ_L), β = Σw/(wᵀΣw), and variance Σ_kk − β_k·(Σw)_k (standard
11
+ * Gaussian conditioning). The rendered component clip(y_k·r(L), 0, 1), r = f(L)/L, then has the closed-form clipped-normal
12
+ * moments for each L, and its expectation is a 1-D integral over L: full, a composite 16-point Gauss–Legendre rule over
13
+ * μ_L ± 8.5σ_L in 4 panels (split at L = 0, where the curves leave a pixel unchanged), the structure of the per-channel Reinhard
14
+ * moments; fast, an 8-point Gauss–Hermite rule in L where the range does not reach L = 0 (else the full rule). The expected
15
+ * slopes (Stein's lemma, for the view's linear noise part) are E[∂out_k/∂y_j] = E[1_in·(δ_kj·r + y_k·r′(L)·w_j)], from the
16
+ * same conditional moments. Derived for this engine from standard Gaussian conditioning; the curves are those of
17
+ * src/output/sdr-rendering.ts (Reinhard et al. 2002; Naka & Rushton 1966).
18
+ * - **Reconstruct and desaturation by a 3-D rule.** The blend of `reconstruct` (src/color/sensor-color-development.ts) depends on
19
+ * the largest camera channel and the clipped channels' largest value, and the desaturation on the largest rendered component:
20
+ * piecewise-smooth functions of all three. Where a pixel's camera channels reach the blend (any channel within 8.5σ of 0.8 of
21
+ * white or above) or, with the desaturation, a rendered component reaches 1, the whole chain (white balance, highlight,
22
+ * matrix, tone curve, desaturation, clip) is integrated over the pixel's normal camera channels with a tensor Gauss–Hermite
23
+ * rule in Cholesky coordinates (nodes and weights from the Jacobi matrix's eigenvalues, Golub & Welsch 1969; NIST DLMF
24
+ * §3.5(v)): full 16 points per axis (4,096 nodes), fast 6 per axis (216). Elsewhere `reconstruct` is the unclipped white
25
+ * balance (exactly linear) and the desaturation leaves the pixel unchanged.
26
+ * The fast rules are the registered fast methods `output.toneCurve/view-luminance-moments` and
27
+ * `output.highlightMode.reconstruct/view-moments` (src/facade/fast-methods.ts); their measured bounds are in docs/DISPLAY_VIEW.md.
28
+ */
29
+ import { portablePow } from "../core/portable-math.js";
30
+ import { CLIP_REACH_SIGMA, GL, bigPhi, phi } from "./display-view-normal.js";
31
+ import { desaturateToLuminance, SDR_LUMINANCE_WEIGHTS } from "../output/sdr-rendering.js";
32
+ import { RAW_HIGHLIGHT_RECONSTRUCTION_START } from "../color/sensor-color-development.js";
33
+ import { NEIGHBOR_HIGHLIGHT_MARGIN, NEIGHBOR_HIGHLIGHT_RING, NEIGHBOR_HIGHLIGHT_SOR, NEIGHBOR_HIGHLIGHT_THRESHOLD, rebuildClippedPixel } from "../color/neighbor-highlight-reconstruction.js";
34
+ /**
35
+ * Internal (exported for tests). The n-point Gauss–Hermite rule of the standard normal: Σ w_i f(x_i) ≈ E[f(Z)], Z ~ N(0, 1).
36
+ * The nodes are the eigenvalues of the Jacobi matrix of the orthonormal Hermite polynomials (zero diagonal, off-diagonal √k),
37
+ * found by Sturm-sequence bisection, and w_i = 1/Σ_k p_k(x_i)² (Golub & Welsch 1969). Only correctly rounded operations, so the
38
+ * rule has the same bits in every JavaScript engine.
39
+ */
40
+ export function gaussHermite(n) {
41
+ const x = new Float64Array(n), w = new Float64Array(n), limit = 2 * Math.sqrt(n) + 1;
42
+ /** The number of eigenvalues below λ (Sturm count of the LDLᵀ pivots). */
43
+ const below = (lambda) => {
44
+ let count = 0, q = -lambda;
45
+ for (let k = 1;; k++) {
46
+ if (q < 0)
47
+ count++;
48
+ if (k === n)
49
+ return count;
50
+ q = -lambda - k / (q === 0 ? 1e-300 : q);
51
+ }
52
+ };
53
+ for (let i = 0; i < n; i++) {
54
+ let lo = -limit, hi = limit;
55
+ for (let it = 0; it < 200 && hi - lo > 0; it++) {
56
+ const mid = (lo + hi) / 2;
57
+ if (mid === lo || mid === hi)
58
+ break;
59
+ if (below(mid) > i)
60
+ hi = mid;
61
+ else
62
+ lo = mid;
63
+ }
64
+ const z = (lo + hi) / 2;
65
+ let p0 = 1, p1 = z, sum = 1 + (n > 1 ? z * z : 0);
66
+ for (let k = 1; k < n - 1; k++) {
67
+ const p2 = (z * p1 - Math.sqrt(k) * p0) / Math.sqrt(k + 1);
68
+ p0 = p1;
69
+ p1 = p2;
70
+ sum += p2 * p2;
71
+ }
72
+ x[i] = z;
73
+ w[i] = 1 / sum;
74
+ }
75
+ // Symmetric rule: the middle node of an odd rule is 0 exactly, and mirrored nodes share their weight.
76
+ for (let i = 0; i < n >> 1; i++) {
77
+ const a = (x[n - 1 - i] - x[i]) / 2, b = (w[i] + w[n - 1 - i]) / 2;
78
+ x[i] = -a;
79
+ x[n - 1 - i] = a;
80
+ w[i] = b;
81
+ w[n - 1 - i] = b;
82
+ }
83
+ if (n & 1)
84
+ x[n >> 1] = 0;
85
+ let total = 0;
86
+ for (let i = 0; i < n; i++)
87
+ total += w[i];
88
+ for (let i = 0; i < n; i++)
89
+ w[i] = w[i] / total;
90
+ return { x, w };
91
+ }
92
+ /** Points per axis of the 3-D rule (full and fast) and of the fast 1-D luminance rule. */
93
+ export const DISPLAY_VIEW_CHAIN_POINTS = /* @__PURE__ */ Object.freeze({ full: 16, fast: 6 });
94
+ export const DISPLAY_VIEW_LUMINANCE_FAST_POINTS = 8;
95
+ /** #602: the most pixels a clipped region's search visits (fast and full); a larger region takes its ratios from the part found. */
96
+ export const DISPLAY_VIEW_NEIGHBOR_REGION_LIMIT = /* @__PURE__ */ Object.freeze({ fast: 1 << 16, full: 1 << 18 });
97
+ /** The tone ratio r(L) = f(L)/L and its derivative of a luminance curve, for L > 0. */
98
+ /** The limit r(0+) of a luminance curve's ratio (where the curves meet the unchanged pixels at L = 0): 1, 0, 1/(σ·g(Lw)) or ∞. */
99
+ function ratioAtZero(curve, p) {
100
+ if (curve === "extended-reinhard-luminance" || !p || !("exponent" in p))
101
+ return 1;
102
+ const q = p;
103
+ return q.exponent > 1 ? 0 : q.exponent < 1 ? Infinity : (1 + q.semiSaturation / q.whiteLuminance) / q.semiSaturation;
104
+ }
105
+ function luminanceRatio(curve, p) {
106
+ if (curve === "extended-reinhard-luminance" && p && !("exponent" in p)) {
107
+ const inverseWhite2 = 1 / (p.whiteLuminance * p.whiteLuminance);
108
+ // f(L) = L(1 + L/Lw²)/(1 + L): r = (1 + L/Lw²)/(1 + L), r′ = (1/Lw² − 1)/(1 + L)².
109
+ return (l, out) => { const d = 1 + l; out[0] = (1 + l * inverseWhite2) / d; out[1] = (inverseWhite2 - 1) / (d * d); };
110
+ }
111
+ const q = p, n = q.exponent, sigma = q.semiSaturation;
112
+ // g(L) = 1/(1 + u), u = (σ/L)ⁿ; f = g/g(Lw); r = g/(g(Lw)·L), r′ = g·(n·u·g − 1)/(g(Lw)·L²).
113
+ const white = 1 / (1 + portablePow(sigma / q.whiteLuminance, n));
114
+ return (l, out) => { const u = portablePow(sigma / l, n), g = 1 / (1 + u); out[0] = g / (white * l); out[1] = g * (n * u * g - 1) / (white * l * l); };
115
+ }
116
+ /**
117
+ * Internal: the coupled rendering of a view, or null when its tone curve is per channel, its highlight mode `clip` and its gamut
118
+ * handling the component clip (the view's own per-component arithmetic then runs, byte for byte as before).
119
+ */
120
+ export function createDisplayViewCoupledRendering(choice, gains, clipLevel, matrix) {
121
+ const luminanceTone = choice.toneCurve === "extended-reinhard-luminance" || choice.toneCurve === "naka-rushton-luminance";
122
+ const neighbors = choice.highlightMode === "reconstruct-neighbors", reconstruct = choice.highlightMode === "reconstruct" || neighbors, desaturate = choice.gamutHandling === "desaturate-to-luminance";
123
+ if (!luminanceTone && !reconstruct && !desaturate)
124
+ return null;
125
+ const reinhard = choice.toneCurve === "positive-reinhard-per-channel", ratio = luminanceTone ? luminanceRatio(choice.toneCurve, choice.toneCurveParameters) : null;
126
+ const ratioZero = ratioAtZero(choice.toneCurve, choice.toneCurveParameters);
127
+ const w0 = SDR_LUMINANCE_WEIGHTS[0], w1 = SDR_LUMINANCE_WEIGHTS[1], w2 = SDR_LUMINANCE_WEIGHTS[2], w = [w0, w1, w2];
128
+ const g = Float64Array.from(gains), L = clipLevel, M = Float64Array.from(matrix.flat()), start = RAW_HIGHLIGHT_RECONSTRUCTION_START;
129
+ const chainRule = gaussHermite(choice.fast ? DISPLAY_VIEW_CHAIN_POINTS.fast : DISPLAY_VIEW_CHAIN_POINTS.full), hermite8 = gaussHermite(DISPLAY_VIEW_LUMINANCE_FAST_POINTS);
130
+ const rr = new Float64Array(2), b = new Float64Array(3), Jb = new Float64Array(9), y = new Float64Array(3), z = new Float64Array(3), Jt = new Float64Array(9), Jz = new Float64Array(9);
131
+ const T = new Float64Array(9), c = new Float64Array(3), out = new Float64Array(3), Jo = new Float64Array(9), chol = new Float64Array(9);
132
+ /** The highlight step b(c) and its slopes Jb = ∂b/∂c (a.e.), as `prepareSensorColorDevelopment` develops. */
133
+ function highlight(c0, c1, c2) {
134
+ const u0 = c0 * g[0], u1 = c1 * g[1], u2 = c2 * g[2];
135
+ Jb.fill(0);
136
+ if (neighbors) {
137
+ // #602: the pixel's region ratios (its own chromaticity when no region is bound: the rebuild then keeps u).
138
+ if (rho)
139
+ rebuildClippedPixel(c0, c1, c2, g, rho, b, Jb);
140
+ else {
141
+ b[0] = u0;
142
+ b[1] = u1;
143
+ b[2] = u2;
144
+ Jb[0] = g[0];
145
+ Jb[4] = g[1];
146
+ Jb[8] = g[2];
147
+ }
148
+ return;
149
+ }
150
+ if (!reconstruct) {
151
+ b[0] = u0 > L ? L : u0;
152
+ b[1] = u1 > L ? L : u1;
153
+ b[2] = u2 > L ? L : u2;
154
+ Jb[0] = u0 > L ? 0 : g[0];
155
+ Jb[4] = u1 > L ? 0 : g[1];
156
+ Jb[8] = u2 > L ? 0 : g[2];
157
+ return;
158
+ }
159
+ const s = c0 > c1 ? (c0 > c2 ? c0 : c2) : (c1 > c2 ? c1 : c2);
160
+ if (!(s > start)) {
161
+ b[0] = u0;
162
+ b[1] = u1;
163
+ b[2] = u2;
164
+ Jb[0] = g[0];
165
+ Jb[4] = g[1];
166
+ Jb[8] = g[2];
167
+ return;
168
+ }
169
+ const sIndex = c0 > c1 ? (c0 > c2 ? 0 : 2) : (c1 > c2 ? 1 : 2);
170
+ const k0 = u0 > L ? L : u0, k1 = u1 > L ? L : u1, k2 = u2 > L ? L : u2, n = k0 > k1 ? (k0 > k2 ? k0 : k2) : (k1 > k2 ? k1 : k2);
171
+ const nIndex = k0 > k1 ? (k0 > k2 ? 0 : 2) : (k1 > k2 ? 1 : 2), nSlope = (nIndex === 0 ? u0 : nIndex === 1 ? u1 : u2) > L ? 0 : g[nIndex];
172
+ const t = s >= 1 ? 1 : (s - start) / (1 - start), tSlope = s >= 1 ? 0 : 1 / (1 - start);
173
+ b[0] = (1 - t) * u0 + t * n;
174
+ b[1] = (1 - t) * u1 + t * n;
175
+ b[2] = (1 - t) * u2 + t * n;
176
+ for (let k = 0; k < 3; k++) {
177
+ const uk = k === 0 ? u0 : k === 1 ? u1 : u2;
178
+ Jb[k * 4] = (1 - t) * g[k];
179
+ Jb[k * 3 + nIndex] = Jb[k * 3 + nIndex] + t * nSlope;
180
+ Jb[k * 3 + sIndex] = Jb[k * 3 + sIndex] + (n - uk) * tSlope;
181
+ }
182
+ }
183
+ /** Tone, gamut and clip of developed y into out, with the slopes Jo = ∂out/∂y (a.e.). */
184
+ function render() {
185
+ Jt.fill(0);
186
+ if (ratio) {
187
+ const l = w0 * y[0] + w1 * y[1] + w2 * y[2];
188
+ if (l > 0) {
189
+ ratio(l, rr);
190
+ const r = rr[0], rp = rr[1];
191
+ for (let k = 0; k < 3; k++) {
192
+ z[k] = y[k] * r;
193
+ for (let j = 0; j < 3; j++)
194
+ Jt[k * 3 + j] = (k === j ? r : 0) + y[k] * rp * w[j];
195
+ }
196
+ }
197
+ else
198
+ for (let k = 0; k < 3; k++) {
199
+ z[k] = y[k];
200
+ Jt[k * 4] = 1;
201
+ }
202
+ }
203
+ else
204
+ for (let k = 0; k < 3; k++) {
205
+ const v = y[k];
206
+ if (reinhard && v > 0) {
207
+ z[k] = v / (1 + v);
208
+ Jt[k * 4] = 1 / ((1 + v) * (1 + v));
209
+ }
210
+ else {
211
+ z[k] = v;
212
+ Jt[k * 4] = 1;
213
+ }
214
+ }
215
+ // #601: toward the luminance until the largest component is 1 (src/output/sdr-rendering.ts, desaturateToLuminance).
216
+ const top = z[0] > z[1] ? (z[0] > z[2] ? 0 : 2) : (z[1] > z[2] ? 1 : 2), zt = z[top];
217
+ if (desaturate && zt > 1) {
218
+ const lp = w0 * z[0] + w1 * z[1] + w2 * z[2];
219
+ if (lp >= 1) {
220
+ out.fill(1);
221
+ Jo.fill(0);
222
+ return;
223
+ }
224
+ const d = zt - lp, a = (1 - lp) / d;
225
+ // ∂a/∂z_j = (−w_j·d − (1 − lp)·(δ_tj − w_j))/d².
226
+ for (let j = 0; j < 9; j++)
227
+ Jz[j] = 0;
228
+ for (let k = 0; k < 3; k++)
229
+ for (let j = 0; j < 3; j++) {
230
+ const da = (-w[j] * d - (1 - lp) * ((j === top ? 1 : 0) - w[j])) / (d * d);
231
+ Jz[k * 3 + j] = w[j] * (1 - a) + (k === j ? a : 0) + (z[k] - lp) * da;
232
+ }
233
+ desaturateToLuminance(z);
234
+ }
235
+ else {
236
+ Jz.fill(0);
237
+ Jz[0] = 1;
238
+ Jz[4] = 1;
239
+ Jz[8] = 1;
240
+ }
241
+ // Jo = diag(inside)·Jz·Jt.
242
+ for (let k = 0; k < 3; k++) {
243
+ const v = z[k], inside = v > 0 && v < 1;
244
+ out[k] = v < 0 ? 0 : v > 1 ? 1 : v;
245
+ for (let j = 0; j < 3; j++)
246
+ Jo[k * 3 + j] = inside ? Jz[k * 3] * Jt[j] + Jz[k * 3 + 1] * Jt[3 + j] + Jz[k * 3 + 2] * Jt[6 + j] : 0;
247
+ }
248
+ }
249
+ function develop(c0, c1, c2) {
250
+ highlight(c0, c1, c2);
251
+ for (let k = 0; k < 3; k++)
252
+ y[k] = M[k * 3] * b[0] + M[k * 3 + 1] * b[1] + M[k * 3 + 2] * b[2];
253
+ render();
254
+ }
255
+ function pixel(c0, c1, c2, result) {
256
+ develop(c0, c1, c2);
257
+ result[0] = out[0];
258
+ result[1] = out[1];
259
+ result[2] = out[2];
260
+ }
261
+ function chain(cm, C, hm, hv, A) {
262
+ // Cholesky factor of C (positive semidefinite; tiny negative pivots are treated as 0).
263
+ chol.fill(0);
264
+ const l00 = Math.sqrt(Math.max(0, C[0])), l10 = l00 > 0 ? C[3] / l00 : 0, l20 = l00 > 0 ? C[6] / l00 : 0;
265
+ const l11 = Math.sqrt(Math.max(0, C[4] - l10 * l10)), l21 = l11 > 0 ? (C[7] - l20 * l10) / l11 : 0, l22 = Math.sqrt(Math.max(0, C[8] - l20 * l20 - l21 * l21));
266
+ const rule = l00 > 0 || l11 > 0 || l22 > 0 ? chainRule : { x: Float64Array.of(0), w: Float64Array.of(1) }, n = rule.x.length;
267
+ let e0 = 0, e1 = 0, e2 = 0, q0 = 0, q1 = 0, q2 = 0;
268
+ T.fill(0);
269
+ for (let i = 0; i < n; i++) {
270
+ const x0 = rule.x[i], wi = rule.w[i];
271
+ for (let j = 0; j < (l11 > 0 || l21 !== 0 || l22 > 0 ? n : 1); j++) {
272
+ const x1 = l11 > 0 || l21 !== 0 || l22 > 0 ? rule.x[j] : 0, wj = l11 > 0 || l21 !== 0 || l22 > 0 ? wi * rule.w[j] : wi;
273
+ for (let k = 0; k < (l22 > 0 ? n : 1); k++) {
274
+ const x2 = l22 > 0 ? rule.x[k] : 0, weight = l22 > 0 ? wj * rule.w[k] : wj;
275
+ c[0] = cm[0] + l00 * x0;
276
+ c[1] = cm[1] + l10 * x0 + l11 * x1;
277
+ c[2] = cm[2] + l20 * x0 + l21 * x1 + l22 * x2;
278
+ develop(c[0], c[1], c[2]);
279
+ e0 += weight * out[0];
280
+ e1 += weight * out[1];
281
+ e2 += weight * out[2];
282
+ q0 += weight * out[0] * out[0];
283
+ q1 += weight * out[1] * out[1];
284
+ q2 += weight * out[2] * out[2];
285
+ // ∂out/∂c = Jo·M·Jb.
286
+ for (let r = 0; r < 3; r++)
287
+ for (let s = 0; s < 3; s++) {
288
+ const ms = M[r * 3] * Jb[s] + M[r * 3 + 1] * Jb[3 + s] + M[r * 3 + 2] * Jb[6 + s];
289
+ for (let o = 0; o < 3; o++)
290
+ T[o * 3 + s] = T[o * 3 + s] + weight * Jo[o * 3 + r] * ms;
291
+ }
292
+ }
293
+ }
294
+ }
295
+ hm[0] = e0;
296
+ hm[1] = e1;
297
+ hm[2] = e2;
298
+ hv[0] = Math.max(0, q0 - e0 * e0);
299
+ hv[1] = Math.max(0, q1 - e1 * e1);
300
+ hv[2] = Math.max(0, q2 - e2 * e2);
301
+ A.set(T);
302
+ return n ** ((l00 > 0 ? 1 : 0) + (l11 > 0 || l21 !== 0 || l22 > 0 ? 1 : 0) + (l22 > 0 ? 1 : 0));
303
+ }
304
+ function reconstructInReach(cm, C) {
305
+ const threshold = neighbors ? NEIGHBOR_HIGHLIGHT_THRESHOLD : start;
306
+ for (let k = 0; k < 3; k++)
307
+ if (!(cm[k] + CLIP_REACH_SIGMA * Math.sqrt(Math.max(0, C[k * 4])) < threshold))
308
+ return true;
309
+ return false;
310
+ }
311
+ // #602: the region ratios of the bound view, found on demand (noise-free channels; see the module notes of the capture's method).
312
+ let rho = null, view = null;
313
+ const ratioOf = new Map(), channelMemo = new Map(), neighborRegions = { measured: 0, capped: 0 };
314
+ const H = NEIGHBOR_HIGHLIGHT_THRESHOLD, limit = choice.fast ? DISPLAY_VIEW_NEIGHBOR_REGION_LIMIT.fast : DISPLAY_VIEW_NEIGHBOR_REGION_LIMIT.full;
315
+ function channelsAt(x, y) {
316
+ const key = y * 1048576 + x;
317
+ let v = channelMemo.get(key);
318
+ if (!v) {
319
+ v = new Float64Array(3);
320
+ view.channels(x, y, v);
321
+ channelMemo.set(key, v);
322
+ }
323
+ return v;
324
+ }
325
+ const inside = (x, y) => x >= view.x0 && y >= view.y0 && x < view.x0 + view.width && y < view.y0 + view.height;
326
+ const isClipped = (v) => v[0] >= H || v[1] >= H || v[2] >= H;
327
+ // A pixel within the margin of a clipped one gives no ratios (the capture's widened regions).
328
+ const unknownMemo = new Map(), D = NEIGHBOR_HIGHLIGHT_MARGIN;
329
+ function isUnknown(x, y) {
330
+ const key = y * 1048576 + x;
331
+ let v = unknownMemo.get(key);
332
+ if (v === undefined) {
333
+ v = false;
334
+ for (let qy = y - D; qy <= y + D && !v; qy++)
335
+ for (let qx = x - D; qx <= x + D && !v; qx++)
336
+ if (inside(qx, qy) && isClipped(channelsAt(qx, qy)))
337
+ v = true;
338
+ unknownMemo.set(key, v);
339
+ }
340
+ return v;
341
+ }
342
+ function ownRatio(v, out) {
343
+ const u0 = v[0] * g[0], u1 = v[1] * g[1], u2 = v[2] * g[2], sum = u0 + u1 + u2;
344
+ if (sum > 0) {
345
+ out[0] = u0 / sum;
346
+ out[1] = u1 / sum;
347
+ out[2] = u2 / sum;
348
+ }
349
+ else
350
+ out.fill(1 / 3);
351
+ }
352
+ function regionRatio(x, y) {
353
+ const key = y * 1048576 + x, known = ratioOf.get(key);
354
+ if (known)
355
+ return known;
356
+ // The region (8-connected), breadth first, up to the cap.
357
+ const members = [key], index = new Map([[key, 0]]);
358
+ let capped = false;
359
+ for (let head = 0; head < members.length; head++) {
360
+ const p = members[head], px = p % 1048576, py = (p - px) / 1048576;
361
+ for (let dy = -1; dy <= 1; dy++)
362
+ for (let dx = -1; dx <= 1; dx++) {
363
+ const qx = px + dx, qy = py + dy, q = qy * 1048576 + qx;
364
+ if (!inside(qx, qy) || index.has(q) || !isUnknown(qx, qy))
365
+ continue;
366
+ if (members.length >= limit) {
367
+ capped = true;
368
+ continue;
369
+ }
370
+ index.set(q, members.length);
371
+ members.push(q);
372
+ }
373
+ }
374
+ neighborRegions.measured++;
375
+ if (capped)
376
+ neighborRegions.capped++;
377
+ // Fast: the ring's mean ratios (unclipped pixels within the ring width, each once).
378
+ const R = NEIGHBOR_HIGHLIGHT_RING, seen = new Set(), r = new Float64Array(3), mean = new Float64Array(3);
379
+ let count = 0;
380
+ for (const p of members) {
381
+ const px = p % 1048576, py = (p - px) / 1048576;
382
+ for (let qy = py - R; qy <= py + R; qy++)
383
+ for (let qx = px - R; qx <= px + R; qx++) {
384
+ const q = qy * 1048576 + qx;
385
+ if (!inside(qx, qy) || seen.has(q) || index.has(q) || isUnknown(qx, qy))
386
+ continue;
387
+ const v = channelsAt(qx, qy);
388
+ seen.add(q);
389
+ ownRatio(v, r);
390
+ mean[0] = mean[0] + r[0];
391
+ mean[1] = mean[1] + r[1];
392
+ mean[2] = mean[2] + r[2];
393
+ count++;
394
+ }
395
+ }
396
+ if (count > 0) {
397
+ mean[0] = mean[0] / count;
398
+ mean[1] = mean[1] / count;
399
+ mean[2] = mean[2] / count;
400
+ }
401
+ else
402
+ mean.fill(1 / 3);
403
+ const values = new Float64Array(3 * members.length);
404
+ for (let i = 0; i < members.length; i++)
405
+ values.set(mean, 3 * i);
406
+ if (!choice.fast && count > 0) {
407
+ // Full: successive over-relaxation of the discrete Laplace equation, Dirichlet values from the unclipped 4-neighbors.
408
+ const { omega, tolerance, maximumIterations } = NEIGHBOR_HIGHLIGHT_SOR, boundary = new Map();
409
+ let change = Infinity;
410
+ for (let sweep = 0; sweep < maximumIterations && change > tolerance; sweep++) {
411
+ change = 0;
412
+ for (let i = 0; i < members.length; i++) {
413
+ const p = members[i], px = p % 1048576, py = (p - px) / 1048576;
414
+ let a0 = 0, a1 = 0, a2 = 0, m = 0;
415
+ for (let d = 0; d < 4; d++) {
416
+ const qx = px + (d === 0 ? -1 : d === 1 ? 1 : 0), qy = py + (d === 2 ? -1 : d === 3 ? 1 : 0), q = qy * 1048576 + qx;
417
+ if (!inside(qx, qy))
418
+ continue;
419
+ const j = index.get(q);
420
+ let src;
421
+ if (j !== undefined)
422
+ src = values.subarray(3 * j, 3 * j + 3);
423
+ else {
424
+ let bnd = boundary.get(q);
425
+ if (!bnd) {
426
+ bnd = new Float64Array(3);
427
+ if (isUnknown(qx, qy))
428
+ bnd.set(mean);
429
+ else
430
+ ownRatio(channelsAt(qx, qy), bnd);
431
+ boundary.set(q, bnd);
432
+ }
433
+ src = bnd;
434
+ }
435
+ a0 += src[0];
436
+ a1 += src[1];
437
+ a2 += src[2];
438
+ m++;
439
+ }
440
+ if (m === 0)
441
+ continue;
442
+ for (let k = 0; k < 3; k++) {
443
+ const old = values[3 * i + k], next = old + omega * ((k === 0 ? a0 : k === 1 ? a1 : a2) / m - old);
444
+ values[3 * i + k] = next;
445
+ if (Math.abs(next - old) > change)
446
+ change = Math.abs(next - old);
447
+ }
448
+ }
449
+ }
450
+ }
451
+ for (let i = 0; i < members.length; i++)
452
+ ratioOf.set(members[i], values.subarray(3 * i, 3 * i + 3));
453
+ return ratioOf.get(key);
454
+ }
455
+ const own = new Float64Array(3);
456
+ function at(x, y) {
457
+ if (!view || !inside(x, y)) {
458
+ rho = null;
459
+ return;
460
+ }
461
+ if (isUnknown(x, y))
462
+ rho = regionRatio(x, y);
463
+ else {
464
+ ownRatio(channelsAt(x, y), own);
465
+ rho = own;
466
+ }
467
+ }
468
+ function bindView(setup) {
469
+ // The noise-free mosaic (expected normalized site values clipped at white, read reflect-101 at the raster's edges) through the
470
+ // bilinear kernels: the expected camera channels of each real pixel (the capture's own demosaic for AHD captures is approximated).
471
+ const W = setup.nativeWidth, Hh = setup.nativeHeight, k = setup.readout.map((r) => r.perElectron), dark = setup.readout.map((r) => r.darkElectrons);
472
+ const site = (x, y) => {
473
+ const sx = x < 0 ? -x : x >= W ? 2 * W - 2 - x : x, sy = y < 0 ? -y : y >= Hh ? 2 * Hh - 2 - y : y;
474
+ const ch = (sy & 1) === 0 ? ((sx & 1) === 0 ? 0 : 1) : ((sx & 1) === 0 ? 1 : 2), v = k[ch] * (setup.signal.site(sx, sy) + dark[ch]);
475
+ return v < 1 ? v : 1;
476
+ };
477
+ view = { x0: setup.margin, y0: setup.margin, width: setup.frameWidth, height: setup.frameHeight, channels(x, y, out) {
478
+ const taps = setup.demosaic[(y & 1) * 2 + (x & 1)];
479
+ for (let c = 0; c < 3; c++) {
480
+ let sum = 0;
481
+ for (const t of taps[c])
482
+ sum += t.weight * site(x + t.dx, y + t.dy);
483
+ out[c] = sum;
484
+ }
485
+ } };
486
+ ratioOf.clear();
487
+ channelMemo.clear();
488
+ unknownMemo.clear();
489
+ neighborRegions.measured = 0;
490
+ neighborRegions.capped = 0;
491
+ }
492
+ const m1 = new Float64Array(3), m2 = new Float64Array(3), sw = new Float64Array(3), beta = new Float64Array(3), cv = new Float64Array(3);
493
+ /**
494
+ * The clipped-normal terms of Z ~ N(m, v) on [0, 1]: P(0 < Z < 1), E[Z·1(0 < Z < 1)], E[Z²·1(0 < Z < 1)] and P(Z ≥ 1), into
495
+ * rr-like slots (q). Closed form, as `unitClip` without its skewness term.
496
+ */
497
+ const q = new Float64Array(4);
498
+ function clipped(m, v) {
499
+ if (!(v > 0)) {
500
+ const inside = m > 0 && m < 1;
501
+ q[0] = inside ? 1 : 0;
502
+ q[1] = inside ? m : 0;
503
+ q[2] = inside ? m * m : 0;
504
+ q[3] = m >= 1 ? 1 : 0;
505
+ return;
506
+ }
507
+ const s = Math.sqrt(v), a = -m / s, bb = (1 - m) / s;
508
+ const Pa = a < -CLIP_REACH_SIGMA ? 0 : a > CLIP_REACH_SIGMA ? 1 : bigPhi(a), Pb = bb > CLIP_REACH_SIGMA ? 1 : bb < -CLIP_REACH_SIGMA ? 0 : bigPhi(bb);
509
+ const da = a < -CLIP_REACH_SIGMA || a > CLIP_REACH_SIGMA ? 0 : phi(a), db = bb > CLIP_REACH_SIGMA || bb < -CLIP_REACH_SIGMA ? 0 : phi(bb);
510
+ const inside = Pb - Pa;
511
+ q[0] = inside;
512
+ q[1] = m * inside + s * (da - db);
513
+ q[2] = (m * m + v) * inside + 2 * m * s * (da - db) + v * (a * da - bb * db);
514
+ q[3] = 1 - Pb;
515
+ }
516
+ function tone(mu, S, hm, hv, J) {
517
+ const muL = w0 * mu[0] + w1 * mu[1] + w2 * mu[2];
518
+ for (let k = 0; k < 3; k++)
519
+ sw[k] = S[k * 3] * w0 + S[k * 3 + 1] * w1 + S[k * 3 + 2] * w2;
520
+ const vL = w0 * sw[0] + w1 * sw[1] + w2 * sw[2], sL = vL > 0 ? Math.sqrt(vL) : 0;
521
+ for (let k = 0; k < 3; k++) {
522
+ beta[k] = sL > 0 ? sw[k] / vL : 0;
523
+ cv[k] = Math.max(0, S[k * 4] - (sL > 0 ? beta[k] * sw[k] : 0));
524
+ }
525
+ m1.fill(0);
526
+ m2.fill(0);
527
+ J.fill(0);
528
+ let reachesTop = false;
529
+ const node = (l, weight) => {
530
+ let r = 1, rp = 0;
531
+ if (l > 0) {
532
+ ratio(l, rr);
533
+ r = rr[0];
534
+ rp = rr[1];
535
+ }
536
+ for (let k = 0; k < 3; k++) {
537
+ const m = mu[k] + beta[k] * (l - muL);
538
+ clipped(r * m, r * r * cv[k]);
539
+ m1[k] = m1[k] + weight * (q[1] + q[3]);
540
+ m2[k] = m2[k] + weight * (q[2] + q[3]);
541
+ if (q[3] > 0)
542
+ reachesTop = true;
543
+ // E[∂out_k/∂y_j | L] = δ_kj·r·P_in + r′·w_j·E[y_k·1_in] (E[y_k·1_in] = E[Z·1_in]/r).
544
+ for (let j = 0; j < 3; j++)
545
+ J[k * 3 + j] = J[k * 3 + j] + weight * ((k === j ? r * q[0] : 0) + (rp === 0 ? 0 : rp * w[j] * q[1] / r));
546
+ }
547
+ };
548
+ if (!(sL > 0))
549
+ node(muL, 1);
550
+ else {
551
+ const lo = muL - CLIP_REACH_SIGMA * sL, hi = muL + CLIP_REACH_SIGMA * sL;
552
+ if (choice.fast && (muL > 2 * CLIP_REACH_SIGMA * sL || hi < 0))
553
+ for (let i = 0; i < hermite8.x.length; i++)
554
+ node(muL + sL * hermite8.x[i], hermite8.w[i]);
555
+ else {
556
+ // Composite 16-point Gauss–Legendre in 4 panels over [lo, hi], split at L = 0 where the curves leave the pixel unchanged; in
557
+ // standardized units z = (L − μ_L)/σ_L, so a node's density is formed without the cancellation L − μ_L.
558
+ const pieces = lo < 0 && hi > 0 ? [-CLIP_REACH_SIGMA, -muL / sL, CLIP_REACH_SIGMA] : [-CLIP_REACH_SIGMA, CLIP_REACH_SIGMA];
559
+ for (let piece = 0; piece + 1 < pieces.length; piece++) {
560
+ const a = pieces[piece], width = (pieces[piece + 1] - a) / 4, half = width / 2;
561
+ for (let p = 0; p < 4; p++) {
562
+ const centre = a + (p + 0.5) * width;
563
+ for (let k = 0; k < 16; k++) {
564
+ const z = centre + half * GL.x[k];
565
+ node(muL + sL * z, half * GL.w[k] * phi(z));
566
+ }
567
+ }
568
+ }
569
+ }
570
+ }
571
+ // Where the luminance's range holds L = 0 and the ratio jumps there (r(0+) ≠ 1), the rendered value jumps across the plane
572
+ // wᵀy = 0, and the expected slopes gain p_L(0)·E[out(0+) − out(0−) | L = 0]·w_j (the jump's term of Stein's lemma).
573
+ if (sL > 0 && ratioZero !== 1 && Math.abs(muL) < CLIP_REACH_SIGMA * sL) {
574
+ const density = phi(muL / sL) / sL;
575
+ for (let k = 0; k < 3; k++) {
576
+ const m = mu[k] - beta[k] * muL, v = cv[k];
577
+ clipped(m, v);
578
+ const below = q[1] + q[3];
579
+ let above;
580
+ if (ratioZero === Infinity)
581
+ above = v > 0 ? bigPhi(m / Math.sqrt(v)) : m > 0 ? 1 : 0;
582
+ else {
583
+ clipped(ratioZero * m, ratioZero * ratioZero * v);
584
+ above = q[1] + q[3];
585
+ }
586
+ for (let j = 0; j < 3; j++)
587
+ J[k * 3 + j] = J[k * 3 + j] + density * (above - below) * w[j];
588
+ }
589
+ }
590
+ for (let k = 0; k < 3; k++) {
591
+ hm[k] = m1[k];
592
+ hv[k] = Math.max(0, m2[k] - m1[k] * m1[k]);
593
+ }
594
+ return !(desaturate && reachesTop);
595
+ }
596
+ return { luminanceTone, reconstruct, neighbors, ...(neighbors ? { bindView, at } : {}), neighborRegions, desaturate, choice, chainPoints: chainRule.x.length, chainRule, luminanceRule: hermite8, ratioAtZero: ratioZero, pixel, chain, reconstructInReach, tone };
597
+ }
598
+ //# sourceMappingURL=display-view-rendering.js.map