@photivra/engine 2.20.0 → 2.21.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 +178 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +7 -0
- package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
- package/dist/capture/exposure-build-up-stage.d.ts +16 -0
- package/dist/capture/exposure-build-up-stage.js +4 -0
- package/dist/capture/exposure-build-up-stage.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +2 -1
- package/dist/capture/native-environment-lean.js +41 -73
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.js +4 -3
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/native-raw.d.ts +3 -2
- package/dist/capture/native-raw.js +5 -4
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +4 -1
- package/dist/capture/sensor-raw-producer.js +12 -3
- package/dist/capture/sensor-raw-producer.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/equipment/generic-lens-catalogue.d.ts +3 -2
- package/dist/equipment/generic-lens-catalogue.js +18 -5
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +32 -0
- package/dist/equipment/generic-tier-assets.js +16 -11
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/sensor-profile-body.d.ts +78 -0
- package/dist/equipment/sensor-profile-body.js +77 -0
- package/dist/equipment/sensor-profile-body.js.map +1 -0
- package/dist/equipment/sensor-profiles.d.ts +8 -0
- package/dist/equipment/sensor-profiles.js +82 -0
- package/dist/equipment/sensor-profiles.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +2 -2
- package/dist/facade/catalogue-lens-signal.js +58 -71
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +2 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +7 -0
- package/dist/facade/defocus-image-space-provider.js +64 -76
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +2 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +6 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.js +25 -23
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.js +5 -23
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +3 -1
- package/dist/facade/expected-signal-setup.js +1 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +47 -14
- package/dist/facade/expected-signal.js +117 -112
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/exposure-build-up.d.ts +70 -0
- package/dist/facade/exposure-build-up.js +164 -0
- package/dist/facade/exposure-build-up.js.map +1 -0
- package/dist/facade/fast-methods.js +9 -1
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +2 -2
- package/dist/facade/generic-lens-capture.js +2 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane-enable.d.ts +7 -0
- package/dist/facade/image-plane-enable.js +17 -0
- package/dist/facade/image-plane-enable.js.map +1 -0
- package/dist/facade/image-plane-lattice-data.d.ts +6 -0
- package/dist/facade/image-plane-lattice-data.js +10 -0
- package/dist/facade/image-plane-lattice-data.js.map +1 -0
- package/dist/facade/image-plane-support.d.ts +33 -0
- package/dist/facade/image-plane-support.js +11 -0
- package/dist/facade/image-plane-support.js.map +1 -0
- package/dist/facade/image-plane.d.ts +36 -0
- package/dist/facade/image-plane.js +353 -0
- package/dist/facade/image-plane.js.map +1 -0
- package/dist/facade/mixed-illumination.d.ts +108 -0
- package/dist/facade/mixed-illumination.js +174 -0
- package/dist/facade/mixed-illumination.js.map +1 -0
- package/dist/facade/representative-light-sources.d.ts +72 -0
- package/dist/facade/representative-light-sources.js +124 -0
- package/dist/facade/representative-light-sources.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +129 -4
- package/dist/facade/scene-radiance.js +48 -13
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +39 -10
- package/dist/facade/simulate-capture.js +252 -92
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +14 -6
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +13 -1
- package/dist/gpu/chart-composite-gpu.js +48 -9
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +4 -30
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
- package/dist/gpu/display-view-composite-material-module.js +39 -13
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-material-module.d.ts +86 -0
- package/dist/gpu/display-view-image-material-module.js +339 -0
- package/dist/gpu/display-view-image-material-module.js.map +1 -0
- package/dist/gpu/display-view-image-quality.d.ts +45 -0
- package/dist/gpu/display-view-image-quality.js +380 -0
- package/dist/gpu/display-view-image-quality.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
- package/dist/gpu/display-view-quality-gpu.js +136 -205
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
- package/dist/gpu/display-view-speed-gpu.js +131 -93
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
- package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
- package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
- package/dist/gpu/wgsl-exact-sum.js +42 -0
- package/dist/gpu/wgsl-exact-sum.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/motion/subject-motion.d.ts +66 -0
- package/dist/motion/subject-motion.js +52 -0
- package/dist/motion/subject-motion.js.map +1 -0
- package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
- package/dist/optics/generic-lens-lateral-color.js +77 -0
- package/dist/optics/generic-lens-lateral-color.js.map +1 -0
- package/dist/optics/generic-lens-psf.js +54 -46
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +7 -0
- package/dist/optics/lens-psf-profile.js +2 -1
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/output/developed-lens-corrections.d.ts +19 -5
- package/dist/output/developed-lens-corrections.js +28 -9
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +41 -5
- package/dist/output/generic-lens-corrections.js +52 -8
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/print-plan.d.ts +41 -5
- package/dist/output/print-plan.js +59 -14
- package/dist/output/print-plan.js.map +1 -1
- package/dist/sensor/binomial-sample.d.ts +3 -0
- package/dist/sensor/binomial-sample.js +86 -0
- package/dist/sensor/binomial-sample.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +10 -0
- package/dist/sensor/sensor-characteristics.js +20 -9
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +98 -1
- package/docs/QUICK_START.md +125 -0
- package/docs/USAGE.md +92 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +10 -5
- package/docs/api/equipment-sensor-profile-body.md +64 -0
- package/docs/api/equipment-sensor-profiles.md +86 -0
- package/docs/api/exports.json +207 -2
- package/docs/api/facade-exposure-build-up.md +105 -0
- package/docs/api/facade-fast-methods.md +9 -1
- package/docs/api/facade-image-plane-enable.md +16 -0
- package/docs/api/facade-mixed-illumination.md +67 -0
- package/docs/api/facade-representative-light-sources.md +131 -0
- package/docs/api/facade-scene-radiance.md +54 -3
- package/docs/api/facade-simulate-capture.md +26 -6
- package/docs/api/motion-subject-motion.md +23 -0
- package/docs/api/optics-generic-lens-lateral-color.md +79 -0
- package/docs/api/output-developed-lens-corrections.md +10 -1
- package/docs/api/output-generic-lens-corrections.md +40 -4
- package/docs/api/output-print-plan.md +35 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,183 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.21.0 — unreleased (prepared November 10, 2026)
|
|
4
|
+
|
|
5
|
+
A minor release that combines the 2.21.0 and 2.22.0 parts (owner decision 2026-11-10): the chart-signal design choice on every render path, sensor profiles and phone lenses (#554), the image-plane scene (#581), catalogue-lens boundary ties (#588) and lateral color (#589), exposure build-up (#534) with subject motion (#551) and mixed lighting (#550), the lateral color fringe width (#580), Photivra's own light-source spectra (#557) and the print planner's contain fit (#365). See the [2.21.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_21_0.md).
|
|
6
|
+
|
|
7
|
+
- **New:** `sensor.profile` and the profile catalogue `SENSOR_PROFILES` / `resolveSensorProfile`, with three phone lenses (#554); the `image-plane` scene and `prepareImagePlaneScenes` (#581); `simulateExposureBuildUp` (#534); `motion.subject` (#551); `illumination` (#550); `calculateChannelEffectiveDispersion` (#589); `calculateLateralColorFringe` (#580, with an optional `channelDispersion`); the representative light sources and `createRepresentativeLightSourceIlluminant` (#557); `fit: { kind: "contain" }` in the print planner (#365).
|
|
8
|
+
- **Changed (shipped images and files; see "Default changes"):** chart signals are integrated from the material spectrum per site on every render path (a design choice; samples unchanged but for rounding, Quality paths slower); catalogue PSF kernels carry the PSF's own centroid (#589); catalogue-lens ties resolve as on the lattice (#588); `output.lensCorrections` and DNG lens-correction opcodes use the capture's own channel dispersion (#589); `PrintPlanInput.fit` is a union (#365).
|
|
9
|
+
- **Root exports:** 41 new, 1,890 → 1,931 (runtime exports 637 → 654): 6 from #554 (4 runtime), 3 from #581 (1 runtime), 6 from #534 (3 runtime), 1 from #551 (type), 5 from #550 (types), 3 from #589 (1 runtime), 4 from #580 (1 runtime), 11 from #557 (7 runtime), 2 from #365 (types). Nothing is removed.
|
|
10
|
+
- **Why minor:** new root exports and options; every default change below keeps the old behavior one option away or is a model correction. #365's union type needs a narrowing where code reads `fit.maximumRelativeAspectError`. Not in this release: #590 (sample counts by format, with the spiky-light 5 nm step) and #591 (the channel-response helper).
|
|
11
|
+
|
|
12
|
+
### Default changes (migration)
|
|
13
|
+
|
|
14
|
+
| What changed | Before (2.20.0) | Now (2.21.0) | Keep the old behavior |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| Chart signals on every render path (design choice, owner decision 2026-11-09) | per-material channel values (and a white-integral shortcut for neutral materials) looked up per site | each site's signal integrated from its materials' spectra when it is read, on the CPU and GPU, capture, display view and ideal reference | no option (same samples but for rounding: 1 sample in 442,368 by one code in the measured Quality view; Quality paths 1.5–14× slower, the owner accepted; GPU Speed emulation about +14 %) |
|
|
17
|
+
| Catalogue PSF kernels (#589) | the cells' quantized centroid | each node kernel's centroid moved onto the PSF's own first moment (coma's tilt plus lateral color): rendered lateral color 96–101 % of the lens model | no option (a model correction; off-axis catalogue captures and views change) |
|
|
18
|
+
| Catalogue fast grid (#589) | at least 4 cells per field interval | at least 10 (no run-time fallbacks; 768-wide fast frames 36–40 % slower) | no option |
|
|
19
|
+
| Catalogue-lens ties (#588) | each tap's own point on the exact method | each tap read at its lattice point; under rolling-shutter rotation (or a moving subject) the per-tap points are `lens.catalogue`'s fast method (fast on screen, full for DNG, JPEG and PNG) | `lens: { catalogue: { kind, fast: true } }` keeps per-tap points there |
|
|
20
|
+
| `output.lensCorrections` lateral color and DNG `lensCorrectionOpcodes` (#589, #587) | channel scales at single wavelengths 610, 540 and 465 nm | each channel's effective dispersion for the capture's own channels and light (reported as `channelDispersion` in both assumptions) | no option; `createGenericLensCorrectionProfile` without `channelDispersion` keeps the single wavelengths |
|
|
21
|
+
| `PrintPlanInput.fit` type (#365) | one record kind | the union `PrintFit` (`confirmed-native-aspect` or `contain`) | narrow on `fit.kind`; `confirmed-native-aspect` plans are byte-identical |
|
|
22
|
+
| Catalogue lenses with `sensor.profile` (#554) | (new option) | framed on the profile's real sensor (its centered region of the output's aspect) | without `sensor.profile` nothing changes |
|
|
23
|
+
| Auto ISO's whole-stop grid (#554) | counted from ISO 100 | counted from the body's lowest ISO | the tiers are unchanged (their lowest is ISO 100) |
|
|
24
|
+
| Image-plane print surround (#581) | (new) | its channel signals integrated from its spectrum like a pixel's | — |
|
|
25
|
+
|
|
26
|
+
### Integration (this release)
|
|
27
|
+
|
|
28
|
+
- **Merged** with `--no-ff` in this order: `release-2.20.0` (to join the squashed 2.20.0 history; tree unchanged), `e221-chart-signals`, `e221-588`, `e221-589`, `e221-554`, `e221-581`, `e222-534` (with #551 and #550 merged in), `e222-580`, `e222-557`, `e222-365`.
|
|
29
|
+
- **Speed versus accuracy (2.20.0's scheme):** #588's rolling-shutter choice resolves through `resolveFast(catalogueFast, fastTop)`, so `diagnostics.fast.requested` is the resolved top-level value with `defaultedFrom`, and PNG counts as an export; #565's and #588's lattice providers are one branch (the two never combine) with one assumption method name, `per-tap-source-points`; the registry gains `lens.catalogue/rolling-shutter-taps` (and #551's `motion.subject/temporal-midpoints`).
|
|
30
|
+
- **Design-around on the merged paths:** #581's GPU Speed adapter integrates the material's spectrum in the kernel (`material_signal`) instead of 581's per-material table; the image-plane print's surround is integrated from its spectrum instead of the white integral; #534's display-view lens signal integrates each material's spectrum at #551's times. Mixed light over an image-plane print is refused (its pixel signals are integrated under one illuminant).
|
|
31
|
+
- **#589 with #587:** the DNG opcodes take the same capture-channel dispersion as `output.lensCorrections`, so they still reproduce the developed correction exactly (as 2.20.0's test requires).
|
|
32
|
+
- **#580:** takes #589's dispersion (owner decision) and is re-measured against the RAW centroid measure.
|
|
33
|
+
- **#554 with #567:** the phone lenses' bokeh (the consumer construction; the 24 mm one has a cat's eye) is covered by the bokeh-value test.
|
|
34
|
+
- **CI capacity (owner-approved):** this release's suite is about 28 % longer (CI test legs 2,612 → 3,350 s), and three of 2.20.0's four coverage shards reached the 20-minute job limit. Coverage now runs as six time-balanced shards in `ci.yml` and `publish.yml` (the `coverage gate` job still merges them and enforces the thresholds on the whole suite), the five chart cases of `display-view-quality-gpu.test.ts` run from their own file, and `test/shard-weights.json` is re-estimated from this pull request's CI times. No test was removed and no job limit was raised.
|
|
35
|
+
- **Size:** one pass for savings, then the capture-worker limit raised by the remaining overage (owner rule for this release): 365,164 → 363,695 B after making #554's profile parser and #550's module static imports (as lazy chunks they split the core modules into shared chunks); limit 356,000 → 363,700 B. The display view's added size stays inside its unchanged budget (46,270 B minified, 18,327 B gzip). See the release record's "Sizes".
|
|
36
|
+
|
|
37
|
+
### Chart scenes: signals integrated from the material spectrum per site
|
|
38
|
+
|
|
39
|
+
- **Changed (design choice, owner decision 2026-11-09): every chart-scene render path integrates each site's signal from its material's spectrum when the site is read.** A chart material is kept as its spectral radiance at the spectral nodes (a neutral one: its reflectance times the white radiance), and a site's signal is formed from the spectra of the materials it reads against its own channel's response (and kernel). No per-material channel value, and no white-integral shortcut for neutral materials, remains:
|
|
40
|
+
- **Capture** (the tabulated RAW path, #394, and #564's large-kernel tiles), **CPU display view** (direct and kernel paths, #563/#569 lens zones) and **ideal reference**: per-node kernels C_c(t, λ) = Σ_members w·A_c(λ)·K_λ(t), and per site the sums of C over the taps of each material it reads, then Σ_m Σ_λ L_m(λ)·P_m(λ) (`MaterialSums`, `src/facade/expected-signal.ts`). `neutralDirect`, `spectralDirect`, the per-material combined kernels and `uniformSite` are gone; a site that reads one material only takes the channel's kernel sums in the same order, so it is the same value.
|
|
41
|
+
- **Catalogue fast method (#568):** grid kernels per spectral node (A_c(λ)·K_{g,λ}), no material class folded in; each tap integrates its material's spectrum against the blend. The blend is taken along y once per grid column and native row.
|
|
42
|
+
- **Display view:** the per-material sample memo is removed (the per-real-pixel memo, within one render, stays).
|
|
43
|
+
- **GPU Speed:** the host uploads the channel responses and the material spectra (rounded once); the kernel integrates Σ_λ A_c(λ)·L_m(λ) per site in blocks of 16 nodes, inside the unchanged 64E site budget (up to 256 nodes).
|
|
44
|
+
- **GPU Quality:** the host uploads the per-node kernel values and the spectra; the site passes accumulate one partial per node and sum the partials and the dark electrons in integer arithmetic (`src/gpu/wgsl-exact-sum.ts`, rounded once), so the (T + 3)·E and blocked (cw + ch + G + 3)·E bounds are unchanged (at most 96 nodes; more fall back to the CPU). The factored neutral table and its material-row binding are gone; the uncertain-tap spread is bounded from the spectra's envelope.
|
|
45
|
+
- **`evaluateChartRadiance`, spectral-integral mode:** the GPU returns each query's material and the host integrates its spectrum against the channel weights per query (the same float64 operations; values unchanged).
|
|
46
|
+
- **Results.** Output samples are unchanged in every measured request but one: in the Quality display view of a 384 × 384 region of the composite chart, 1 of 442,368 samples moves by one code (an expected value's last bits crossing a code boundary). Float64 internals move by rounding: 11 display-view hash locks re-recorded (x64 with x64 Node 24.1.0 under Rosetta 2, which reproduces every earlier x64 lock; arm64 natively), expected values within 1.9·10⁻¹⁴ relative, except the #444 AHD case, where 30 of 360 expected components at a patch edge move by at most 0.025 8-bit codes (AHD's choice is now taken where the two candidates' means were equal to the last bit). Every GPU case stays inside its bound (emulation).
|
|
47
|
+
- **Cost** (Node, one thread, measured before and after interleaved in one run; the app's request shape: 6000 × 4000, 6 µm, 2 × 2 box, f/8, Quality with diffraction, the four-spot filter and AHD; the app's composite chart, all neutral, and the spectral `color-patches` chart; 21 spectral nodes):
|
|
48
|
+
- Capture 24 MP, Speed: 11.2 → 11.5 s (composite), 9.4 → 9.6 s (patches). Quality: 13.6 → 96.3 s, 10.9 → 15.9 s.
|
|
49
|
+
- 48-pixel quick frame, Speed: 21 → 23 ms, 20 → 20 ms. Quality: 65 → 68 ms, 64 → 65 ms.
|
|
50
|
+
- Display view, a 384 × 384 region of a 2028 × 1352 view, Speed: 1.06 → 1.06 s, 0.62 → 0.79 s. Quality: 2.7 → 33.9 s, 1.10 → 2.07 s.
|
|
51
|
+
- Display view, a 48-pixel view of the 24 MP frame, Speed: 0.73 → 0.93 s, 0.43 → 0.54 s. Quality: 5.6 → 76.7 s, 2.4 → 6.4 s.
|
|
52
|
+
- Ideal reference, 24 MP: 4.1 → 5.8 s, 2.9 → 3.1 s.
|
|
53
|
+
- Catalogue fast method (#568), 768-pixel quick frame: 193 → 242 ms (Siemens star), 207 → 242 ms (patches).
|
|
54
|
+
- GPU float32 emulation, a 384 × 384 Speed region: 0.93 → 1.07 s (checkerboard), 6.2 → 6.3 s (patches); Quality site pass of a 2028 × 32 band: 1.0 → 5.2 s (pixel pass unchanged, 14 s).
|
|
55
|
+
- The Quality paths integrate N products per kernel tap (400 taps at f/8) instead of reading one value, as the owner accepted.
|
|
56
|
+
- **Size.** The app's capture-worker shape measures 351,992 B gzip (main: 351,775 B; limit 352,000 B, not raised).
|
|
57
|
+
- **Tests:** `test/exact-sum.test.ts` (new); hash locks in `test/display-view.test.ts`, `test/display-view-pixel-memo.test.ts`, `test/display-view-speed-material.test.ts`.
|
|
58
|
+
|
|
59
|
+
### #588 Catalogue lenses: read PSF quadrature taps at lattice points (chart-boundary ties)
|
|
60
|
+
|
|
61
|
+
- **Fixed:** `simulateCapture` with `lens.catalogue` (the exact method, the environment PSF quadrature) formed each tap's source point from its own site's arithmetic, so a tap exactly on a chart boundary (a Siemens star's diagonals through a symmetric frame's center, its vertical spokes through an odd frame's center column) could resolve to the other material than on the lattice that the fast method (#568), the display view (#569) and #548's image-space provider read. The quadrature's provider is now #548's image-space provider with a one-tap kernel (as #565 does for `lens.diffraction`), so each tap reads the scene at its lattice point; the fast method already read the lattice. A catalogue fisheye's lattice points beyond the horizon (outside the 180° image circle) read zero radiance, as the provider does (new internal `zeroBeyondHorizon` input of the image-space provider). With rotation under `motion.shutter: "rolling"` the reading follows `fast` (below).
|
|
62
|
+
- **Which results change:** exact-method catalogue captures, only at such ties. On a 20 × 14 Siemens star at 60 µm, *PV 24 1.8* wide open: 20 of 352 native sites (largest 15× the lattice value); 21 × 14: 8 sites (0.7 %); *PV 8 3.5 Fisheye Pro*: 18 and 8 sites. Elsewhere the expected electrons agree to 2.4e-15. No API, option or default change; `src/gpu` unchanged.
|
|
63
|
+
- **Rolling shutter with camera rotation (owner decision, 2.21.0):** the per-tap points are `lens.catalogue`'s fast choice and the lattice reading its full method, folded into the existing `fast` override (no new option name): the record's `fast`, else the top-level `fast`, else fast for `srgb-8`/`srgb-16` and full for `output.format` `"dng"`, `"jpeg"` or `"png"`. The field-grid method never runs with rotation, so there `diagnostics.fast.applied["lens.catalogue"]` is `true` exactly when the per-tap points ran (`assurance: "approximation-with-a-priori-bound"`, assumption `fast.lens.catalogue` with `method: "per-tap-source-points"`, as for `lens.diffraction`); `requested` follows 2.20.0's scheme (the resolved top-level value), so the on-screen default reports `requested: true, defaultedFrom: "format", applied: true`. The integration shares one lattice provider with #565's `lens.diffraction` branch. The lattice reading there costs 1.07–1.09× the per-tap time (docs/LENS_CATALOGUE.md). Which results change: on-screen captures keep 2.19.0's per-tap RAW; DNG, JPEG and PNG captures of this case now read the lattice (they differ only at ties). Joined parts match the whole frame only when every part reads the same way: set `fast` explicitly. Nothing else changes: rotation with a global shutter always reads the lattice, and the field-grid method's resolution is unchanged.
|
|
64
|
+
- **Cost:** in a CPU profile of a 96 × 64 exact capture (*PV 50 1.8*, f/8, 20 nm) the lattice reader takes about 2.5 % of the time; interleaved wall-clock runs (load average 6–78 from other work) did not separate it from noise (median ratios 0.95–1.03). The image-space provider now replaces an engine-built base result's radiance in place instead of copying the result per query (saves about 1 % there; also for `lens.defocus` in image space).
|
|
65
|
+
- **Bundle:** capture worker +150 B gzip on 2.19.0 (351,775 → 351,925 B; limit 352,000 B, not raised): the lattice branch +117 B and the rolling-shutter fast choice +118 B, less 38 B from sharing the image-space provider's lattice-point mapping and per-node sampling between its band and run-lattice paths and one error site (values bit-identical), and the catalogue's two fast-report updates sharing one function. The display view's added size: 18,153 → 18,154 B gzip (budget 18,550 B); the capture entry chunk 261,002 → 261,167 B.
|
|
66
|
+
- **Tests and docs:** `test/simulate-capture-lens-catalogue-ties.test.ts` (new; *PV 24 1.8* and *PV 8 3.5 Fisheye Pro*, 20 × 14 and 21 × 14 stars: the exact method's RAW equals the lattice reading's, the fast method's sum with exact kernels, and the per-tap points differ at ties; fails on 2.19.0; and, with rolling rotation, the default by format, every explicit override, the report, and equal RAW on dead leaves through both lenses). docs/LENS_CATALOGUE.md ("Ties on chart boundaries"), docs/DISPLAY_VIEW.md (the catalogue section's tie note), docs/API_STYLE.md ("Exact and fast modes"), API reference regenerated (`SimulateCaptureLensCatalogueRecord`).
|
|
67
|
+
|
|
68
|
+
### #589 Catalogue lenses: check rendered lateral color against the lens model on large-blur corners
|
|
69
|
+
|
|
70
|
+
- **Measured:** the blue-minus-red displacement in RAW, independent of blur and demosaicing: the per-CFA-channel centroid of an isolated lattice-point source in a 6 µm region's expected electrons (summed over the four points of a 2 × 2 cell, so each channel sees every kernel tap), fast and exact. Against the lens model at the channels' effective dispersion (below; 0.833 of the single-wavelength L·R·H·(D(465) − D(610)) for the default sensor), *PV 24 1.8* wide open rendered 101 % / 109 % (x / y) at the corner and 121 % / 83 % mid-field, *PV 24 1.4 Pro* 73 % / 38 % and 19 % / 12 %. The large-blur consumer corner was not short of lateral color; the site cells quantized a narrow PSF's centroid and the kernel window's truncation moved a wide one's.
|
|
71
|
+
- **Fixed (owner decision):** each catalogue PSF node kernel's centroid is moved (a linear-interpolation shift between neighboring taps) onto the PSF's own first moment, the pupil-mean wavefront tilt 2·N_w·⟨∇W⟩ (coma's W131·H) plus the lateral color L·R·H·D(λ) (Fourier moment theorem; Goodman, *Introduction to Fourier Optics*, ch. 4). Now 96–101 % of the model for both lenses, corner and mid-field. On-axis kernels are unchanged. A cost-free accuracy gain in time (the profile builds in the same time), so no `fast` choice; an approximation with a blur cost: a move of f sites adds f·(1 − f) sites² of variance, so the 6 µm corner edge rise went from 1.09 to 1.32 px (pro) and from 1.44 to 1.56 px (consumer); the center is unchanged.
|
|
72
|
+
- **Channel effective dispersion (owner decision):** new `calculateChannelEffectiveDispersion({ wavelengthsNm, channelResponse, illuminant })` (approximation, model `generic-lens-channel-effective-dispersion` 1.0.0) gives each channel's D̄_c = Σ response·illuminant·D(λ) / Σ response·illuminant. `createGenericLensCorrectionProfile` takes it as the optional `channelDispersion` (scales 1 + L·D̄_c; without it, D at 610, 540 and 465 nm as before). `output.lensCorrections` now uses the capture's own channels and illuminant and reports `channelDispersion` in its assumption: for the default sensor the red scale is 1 + 0.20·L instead of 1 − 0.11·L, the blue 1 + 1.04·L instead of 1 + 0.90·L. `GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM` is unchanged and remains the profile's default. Root exports +3 (1,860 → 1,863): `calculateChannelEffectiveDispersion` (runtime), the types `CalculateChannelEffectiveDispersionInput` and `ChannelDispersion`.
|
|
73
|
+
- **Which results change:** every `lens.catalogue` capture and display view off the axis, and every capture with `output.lensCorrections.lateralColor` (its channel scales and fill). The chart measures (docs/LENS_CATALOGUE.md): corner rise above; coma 0.02 → 0.10 px (pro), 0.36 → 0.35 px (consumer); the developed corner edge's blue-minus-red 0.31 → 0.44 px (pro), 0.71 → 0.68 px (consumer). Hash lock `catalogue50` in `test/program-line-lens-range.test.ts`: samples and codes changed (off-axis kernels), exposure and assumptions unchanged.
|
|
74
|
+
- **Fast method (owner decision):** the field grid's minimum subdivisions go from 4 to 10 cells per interval (`CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS`, internal), so the run-time verification never falls back. Measured over 3,600 whole frames (every lens, widest and narrowest f-number, 14 charts, 192–6000 pixels wide): with 4 cells and aligned kernels 15 fell back to exact (0.42 %, all *PV 15 2.8 Fisheye Pro* at f/2.8 on 192 pixels), with 10 none (worst 8.0e-4 of the 5e-3 tolerance). Against the exact kernels: at most 2.0e-4 of the peak and 3.4e-3 of a site (the test's 1e-3 and 5e-3 bounds kept); 3.9 % of the codes differ at 375 µm (bound 2 % → 5 %). Cost: 768-wide fast captures 36–40 % slower (2.2–2.5× `lens.prime`, past #568's 2× target); 6000 × 4000 unchanged (25.2 s against 24.7 s). Fast results change accordingly; `fast.lens.catalogue` reports `minimumSubdivisions: 10`.
|
|
75
|
+
- **Why the developed edge differs from the model:** the channels' effective wavelengths, the color matrix that undoes their spectral overlap (it amplifies the blue image's offset from the green), the 50 % point being the line spread function's median under an asymmetric PSF, and the demosaic (docs/LENS_CATALOGUE.md, "Lateral color in RAW").
|
|
76
|
+
- **Bundle:** capture worker 351,775 → 351,997 B gzip (+222 B; limit 352,000 B, not raised). Added: the alignment, the channel dispersion and the two-step correction set-up. Saved, with bit-identical results: row FFTs in place, the phase-step scan in one loop, the window check without a running sum and mirroring folded into the crop (`src/optics/generic-lens-psf.ts`); one kernel centroid function shared by the PSF builder, the fast grid and the profile interpolation (`kernelCentroid`, now exported internally from `src/optics/lens-psf-profile.ts`); the lens corrections' corner vignetting read from the already resolved lens instead of resolving it again. Display view's added size unchanged.
|
|
77
|
+
- **Tests and docs:** `test/simulate-capture-lens-catalogue-lateral-color.test.ts` (new: the RAW measure for both lenses, corner and mid-field, fast and exact, which fails on 2.19.0; every node kernel's centroid on all 30 lenses at 1.5, 6 and 24 µm within 0.005 sites of the first moment; the helper's values and refusals, and the capture reporting the dispersion it corrects with); `test/simulate-capture-lens-catalogue.test.ts` measured values (its lateral check now pro < consumer on the developed edge); `test/simulate-capture-lens-corrections.test.ts` (the profile with the capture's reported dispersion equals its scales). docs/LENS_CATALOGUE.md ("Lateral color in RAW (#589)", "Channel effective dispersion", the measured and fast tables), docs/DIGITAL_OPTICS_FOUNDATION.md (the correction mapping), API reference regenerated.
|
|
78
|
+
|
|
79
|
+
### #554 Sensor profile catalogue
|
|
80
|
+
|
|
81
|
+
- **New (#554): `sensor.profile`** for `simulateCapture` and `simulateDisplayView`: a sensor profile record (`SensorProfile`, checked strictly: every field, no unknown field, ranges, ISO markings, the dual-gain fields). Eight representative, unnamed profiles (`SENSOR_PROFILES`, `SENSOR_PROFILE_IDS`, `resolveSensorProfile(id)`) come from manufacturer-published specifications and an open paper, every value labelled published, derived or design; their sources are owner-approved (2026-11-09) and linked in docs/SENSOR_PROFILES.md. The capture never imports the catalogue (owner, 2026-11-09): the caller passes the record, so a capture worker does not bundle it. A profile replaces the body's ISO range, readout regimes (full well, read noise of each conversion gain, dual-gain switch, high-gain charge storage, ADC bit depth and black level), readout time and fixed-pattern noise, and sets the default pitch and peak QE; the #555 characteristics override it one at a time. The record's parser and body builder are a lazy chunk. Recorded in the assumption `sensor.profile`.
|
|
82
|
+
- **New (#554): three phone lenses in the generic lens catalogue**, a third set `"phone"`: PV 13 2.2 Phone, PV 24 1.8 Phone and PV 77 2.8 Phone (`pv-13-2.2-phone`, `pv-24-1.8-phone`, `pv-77-2.8-phone`; the owner's 13, 24 and 77 mm equivalents at fixed f/2.2, f/1.8 and f/2.8), with a phone set baseline and adjustments that are design values. They render only with a profile whose `fixedLenses` is `"phone"`, and such a profile takes only them. A fixed-aperture lens now defaults `lens.fNumber` to its one f-number (before, f/8 and a refusal). The catalogue has 33 lenses (`GENERIC_LENS_CATALOGUE`).
|
|
83
|
+
- **New (#554): catalogue lenses on a profile's real sensor** (owner, 2026-11-09). With `sensor.profile`, `lens.catalogue` is resolved on the profile's whole sensor (real focal length and field of view) and the capture is its largest centred region of the output's aspect (pitch = min(W/w, H/h); the other axis cropped, never stretched), or the centred crop an explicit pitch gives (a frame beyond the sensor is refused). A site of N photosites sums their charge and their read noise in quadrature (×√N; also explicit `readNoise`); `sensor.fixedPatternNoise` needs the profile's own pitch. Reported in the assumption `sensor.profileView`; the exact and fast catalogue paths and the display view agree. Without a profile nothing changes.
|
|
84
|
+
- **Shared:** #555's range checker (`checkFieldRanges` in `src/sensor/sensor-characteristics.ts`) now also checks the profile record (a predicate and a required mode added); #555's messages are unchanged.
|
|
85
|
+
- **ISO rating (proposal, owner question):** each profile's base ISO is re-rated from its full well, QE and pixel area, S0 = 100·(η/0.5)·(A/36 µm²)·(60 000 e⁻/FW), rounded to a third-stop marking (`SENSOR_PROFILE_ISO_ANCHOR`): the RAW white at base ISO is the full well and the engine's ISO anchor holds on every profile (the rounding, at most 1/6 stop, is `sensitivityOffsetStops`). The high gain's charge storage is the white charge at the switch ISO. A profile whose range starts above ISO 100 defaults to its base ISO.
|
|
86
|
+
- **Changed:** Auto ISO's whole-stop grid counts from the body's lowest ISO (unchanged for the tiers, whose lowest is ISO 100). The tiers' body builder takes a profile's ISO settings and provenance (tier assets byte-identical).
|
|
87
|
+
- **Root exports:** 6 new (4 runtime: `SENSOR_PROFILES`, `SENSOR_PROFILE_IDS`, `SENSOR_PROFILE_ISO_ANCHOR`, `resolveSensorProfile`; 2 types: `SensorProfile`, `SensorProfileValueKind`); `GenericLensSet` gains `"phone"`. Requests without `sensor.profile` and without a phone lens are byte-identical.
|
|
88
|
+
- **Docs and tests:** new docs/SENSOR_PROFILES.md; THIRD_PARTY.md (source review signed off), docs/LENS_CATALOGUE.md ("Phone lenses"), docs/QUICK_START.md, docs/USAGE.md, docs/GENERIC_TIER_PRESETS.md, README.md; `test/sensor-profiles.test.ts` (per profile: pitch, saturation, read noise, readout, ADC, dynamic range = full well ÷ read noise ⊕ quantization at base and switch ISO; the ISO anchor across profiles; overrides; fixed pattern; display view; no maker names; strict record checks; a modified record; the phone lenses on the phone profile only), `test/generic-lens-catalogue.test.ts` (33 lenses, fixed apertures).
|
|
89
|
+
- **Bundle:** see the part report for the capture-worker delta over 2.20.0 part A (limit 352,000 B). The catalogue data are not in it.
|
|
90
|
+
|
|
91
|
+
### #581 Image-plane scene: a photo as a flat print in front of the camera
|
|
92
|
+
|
|
93
|
+
- **New: `prepareImagePlaneScenes()` and the `image-plane` scene.** Call `await prepareImagePlaneScenes()` once: it loads the scene's module and lattice data (a lazy chunk of 14,212 B gzip, and 7,857 B more for the GPU code; about +22.6 KB in all on the worker shape). A bundle that never imports it carries none of those bytes. Then the scene is (`{ kind: "image-plane", sceneId, image: { width, height, channels: 3 | 4, encoding: "srgb-8" | "srgb-16" | "linear-srgb", data }, paper?, interpolation?, maximumPixels? }`, types `ImagePlaneScene`, `ImagePlaneImage`). An sRGB (8- or 16-bit) or linear image prints as a flat matte Lambertian print on the scene plane, lit by the request's illuminant. It is taken by `simulateCapture`, `simulateIdealReference`, `simulateDisplayView` and `prepareDisplayView`, with exposure, noise, diffraction, defocus of the plane, distortion and vignetting acting on it. `createPlanarSceneRadianceProvider` refuses it. See docs/IMAGE_PLANE.md.
|
|
94
|
+
- **Method (owner decision 2026-11-09): the print reflectance lattice.**
|
|
95
|
+
- **Lattice.** 5 × 5 × 5 nodes at the sRGB code values 0, ¼, ½, ¾ and 1. The nodes are flat on the neutral axis; the other 120 are Photivra's own fits of Burns' LHTSS method (arXiv:1710.05732 v5, pp. 24–26, implemented from the equations; the CC BY-SA code is not used). They are fitted against CIE D65 and the CIE 1931 2° observer at fit time only (`scripts/fit-image-plane-lattice.mjs`) and stored at 10 nm over 380–780 nm in 16 bits.
|
|
96
|
+
- **Per pixel.** Each pixel is the trilinear interpolation of its cell's spectra in linear sRGB, then the paper: ρ = black + (white − black)·ρ. Defaults white 0.90 and black 0.01, labeled assumptions.
|
|
97
|
+
- **Round trip under D65** (the fit's check over a 17³ grid of sRGB codes): ΔE00 at most 6.4·10⁻⁴. Every spectrum stays in [0, 1].
|
|
98
|
+
- **Under other lights** the print is a metamer. The spread is measured on synthetic spectra and recorded in the docs, and the result is labeled an approximation (assumption `scene.imagePlane`).
|
|
99
|
+
- **No `fast` option.** The lattice is piecewise linear in RGB, so the tabulated paths stay exact.
|
|
100
|
+
- **Input and placement.** Alpha is composited over the paper. Linear samples outside [0, 1] are clipped and counted (`scene.parameters.clippedSampleCount`, assumption `scene.image.clippedSamples`). The image is limited to 24 MP by default, raised with `maximumPixels`. Reconstruction is bilinear by default (the pixels' spectra, so bilinear in linear sRGB under D65), with `nearest` as the option. `chartSize` sets the print, its height defaulting to the image's aspect; without it the print is fitted inside the frame on the 18 % surround. The scene digest covers the image's SHA-256.
|
|
101
|
+
- **GPU (owner decision 2026-11-09: Speed and Quality).** `prepareDisplayView` renders photos on WebGPU in both display-view paths, so a photo falls back to the CPU only for the reasons a chart does.
|
|
102
|
+
- **Speed.** A new image-plane module (`src/gpu/display-view-image-material-module.ts`). The Speed kernel's site lookup became `scene_value(p, c)`; material modules get a table adapter with the same value, so chart bytes are unchanged. The module's bound is ten extra roundings (γ_74 instead of γ_64 for that module).
|
|
103
|
+
- **Banded pixel data (owner decision 2026-11-09).** A photo whose 12-byte-per-pixel signals exceed one storage binding uploads per band only the pixel rows the band reaches, and the renderer caps a band's rows so they fit. Bounds and self-check are unchanged; a 2400 × 1600 photo under a 16 MB limit renders byte-identical to the whole upload.
|
|
104
|
+
- **Host memory (owner decision 2026-11-09).** The host builds the pixel signals 4 MiB of rows at a time, for the whole upload and per band, and keeps none: a 24 MP photo needs about 4 MiB for them instead of 288 MB per scene (576 MB with the two kept scenes). Every byte is unchanged (the same float64 operations, the lattice cell found once per pixel). A banded 24 MP render builds its rows each time.
|
|
105
|
+
- **Signals are integrated from the reconstructed spectrum per site (design choice 2026-11-09).** Every path that forms a sensor signal from a photo (capture, ideal reference, display view on the CPU, GPU Speed and Quality) integrates the pixel's reconstructed spectrum (its cell's node spectra blended at each spectral node, then the paper, times the illuminant) against the reading site's channel response when it is read; no per-channel values of the lattice nodes are formed. `ResolvedImagePlaneScene.channelTables` is replaced by `pixelSignals` (internal). Output bytes of the measured requests are unchanged; bounds are unchanged and every GPU case stays inside them.
|
|
106
|
+
- **Cost** (Node, one thread, 6000 × 4000 photo, 24 MP 6 µm frame, 20 spectral nodes): capture 24 MP 13.6 → 12.6 s, 48-pixel quick frame 0.08 s both; display view of a 384 × 384 region 0.95 → 0.92 s, a 48-pixel view of the 24 MP frame 1.16 → 1.39 s. GPU Speed's host side on the emulation: the whole upload's one-time build 0.75 → 1.8 s, a banded 24 MP render 2.0 → 2.8 s.
|
|
107
|
+
- **Recovered host time (owner decision 2026-11-09).** Each pixel's three channel sums are formed in one pass over its spectrum (the same sums in the same order), and a band's rows shared with the previous band are copied on the device from that band's buffer, which is kept until the next band; only the other rows are built, so a banded render builds each photo row it reaches once (it built 1.6× before). The bytes are unchanged, the host still holds at most one 4 MiB chunk of signals, and the device holds at most two band buffers at once. Before this, the one-time build took 2.2 s and a banded 24 MP render 4.5 s.
|
|
108
|
+
- **Quality.** Diffraction, `lens.defocus`, the anti-aliasing filter's spots and AHD, through the print's own passes 1 and 2 (`src/gpu/display-view-image-quality.ts`), behind a hook of `renderDisplayViewQualityOnGpu`.
|
|
109
|
+
- Pass 1: the radiance per spectral node at each lattice point, from per-band pixel lattice records.
|
|
110
|
+
- Pass 2: the blocked sum against the reference's per-node combined kernels, bounded at (32 + N + cw + ch + G + 4)·E relative.
|
|
111
|
+
- Flagged lattice points are resolved on the host with the reference's radiance when there are site kernels; without kernels their sites' pixels are recomputed.
|
|
112
|
+
- The pixel pass and finishing are unchanged.
|
|
113
|
+
- **Self-check.** The module checks itself (Speed and Quality) on a device's first photo; the renderer's own cases are unchanged.
|
|
114
|
+
- **Measured** against the CPU reference on the emulation and SwiftShader, in 11 Speed and Quality cases plus two print-edge cases: ΔE2000 at most 7.3·10⁻⁴ (Quality) and 4.2·10⁻⁵ (Speed), every component inside its bound, samples within one code. The SwiftShader display-view suite passes all 69 tests.
|
|
115
|
+
- **Size.** The app's capture-worker shape measures 351,950 B gzip (2.19.0: 351,775 B; limit 352,000 B, not raised).
|
|
116
|
+
- The scene's module, its lattice data and its GPU code load only through `prepareImagePlaneScenes()`, which that shape does not import. An app that calls it adds about 22.6 KB.
|
|
117
|
+
- The GPU display-view chunk is 54,591 B, 116 B below 2.19.0, because its photo routing and banding are paid for by sharing code: the self-checks' fixtures and verdict, the support checks, the display-pixel recomputation, the lattice formula and the materials' channel signals. The entry is +283 B (the facade's dispatch).
|
|
118
|
+
- **Root exports:** 3 new (`prepareImagePlaneScenes`, types `ImagePlaneScene`, `ImagePlaneImage`).
|
|
119
|
+
- **Tests:** `test/image-plane.test.ts`, `test/display-view-image-gpu.test.ts` (float32 emulation), image-plane cases in `test/webgpu/display-view.webgpu.ts` (SwiftShader).
|
|
120
|
+
|
|
121
|
+
### #534 Exposure build-up
|
|
122
|
+
|
|
123
|
+
- **New (#534): `simulateExposureBuildUp(options, { fractions })`** renders fractions of one exposure as one accumulating process, like a camera's live readout of a long exposure.
|
|
124
|
+
- **Signal:** each fraction fⱼ < 1 is a capture of the request in manual mode at the whole exposure's resolved f-number and ISO and the shutter fⱼ·t. Its expected signal is integrated from the spectrum over [0, fⱼ·t] by the capture itself, so camera rotation, rolling shutter, `motion.subject` (#551) and mixed lighting (#550, which does not change over time) are supported (owner decision 2026-11-09). No per-channel signal is precomputed.
|
|
125
|
+
- **Counts:** going down the fractions, each site's photo and dark counts are split as N(fⱼ) ~ Binomial(N(fⱼ₊₁), λ(fⱼ)/λ(fⱼ₊₁)), with λ that render's expected mean at the site. This is the conditional law of a Poisson process with any rate over time. Each fraction's counts are therefore Poisson with its own mean (a capture's mean and variance at fⱼ·t), never decrease, and covary as the increment model says. Where the temporal quadrature puts an earlier mean above a later one, the count is kept and counted (`nonMonotoneSites` in the assumption `exposure.buildUp`).
|
|
126
|
+
- **Sensor:** fixed-pattern noise is the same in every fraction. Each fraction is clipped at the full well and read out once with its own read-noise stream, dual gain, pre-ADC/ADC saturation and ADC bits (#555; #554 profiles feed the same stage).
|
|
127
|
+
- **Byte-identity:** the fraction at 1 is `simulateCapture(options)` byte for byte, including its plan and assumptions (owner decision 2026-11-06).
|
|
128
|
+
- **Reports:** a fraction's `exposure` and `sensor` reports are the whole exposure's (owner decision 2026-11-09).
|
|
129
|
+
- **Limits:** on screen only, `"srgb-8"` and `"srgb-16"`; other formats are refused (owner decision 2026-11-09).
|
|
130
|
+
- **Fast:** the request's `fast` applies to every fraction. The build-up adds no fast method, so there is nothing to register.
|
|
131
|
+
- **Internal:** an exact seeded binomial sampler (`src/sensor/binomial-sample.ts`: inversion below a mean of 10, Hörmann's BTRS above; not a root export). The sensor stage takes an optional build-up stage (`src/capture/exposure-build-up-stage.ts`) in `calculateNativeRawSite`, the native RAW loop, the lean templates and the tabulated path. The stage is found by the native RAW input the facade builds (a callback of the internal `runCapture` sees it) and gets each site's expected means and ordinary draws. Without a stage, every path is unchanged.
|
|
132
|
+
- **Merge of #551 and #550 (here; owner decision 2026-11-09):** `motion.subject` works with `illumination`. #551's revealed 18 % background takes the plane's irradiance at each revealed point: the blend and #550's point sources, from the spectrum at use, in the capture and in the display view. The display view still refuses a rolling shutter and a subject rotation, as for any moving subject. Tests: `test/subject-motion-mixed-lighting.test.ts` (gradient blend and point source against a still 18 % scene under the same lighting, the display view, the build-up with both). +13 B gzip in the capture worker.
|
|
133
|
+
- **Capture-worker refactor (results bit-identical):** the lean sensor templates use the flat sensor stage (#449) instead of a second copy of the same stage (`leanSensorSite` removed), and one `reproducesSite` check serves both verifications.
|
|
134
|
+
- **Root exports:** 6 new (3 runtime): `simulateExposureBuildUp`, `EXPOSURE_BUILD_UP_MAXIMUM_FRACTIONS`, `EXPOSURE_BUILD_UP_STREAM_BASE` and the types `SimulateExposureBuildUpOptions`, `SimulateExposureBuildUpFrame`, `SimulateExposureBuildUpResult`.
|
|
135
|
+
- **Bundle:** #534 alone on 2.20.0 shrinks the capture-worker shape by 134 B gzip (355,994 → 355,860 B). This branch, with #551 and #550 (point sources included) merged, measures 359,781 B.
|
|
136
|
+
- **Docs and tests:** docs/QUICK_START.md ("Exposure build-up (#534)"), docs/USAGE.md, API reference; `test/exposure-build-up.test.ts`, `test/binomial-sample.test.ts`.
|
|
137
|
+
|
|
138
|
+
### #551 Subject motion
|
|
139
|
+
|
|
140
|
+
- **New:** `motion.subject` (type `SimulateCaptureSubjectMotion`): the whole scene, or one declared rectangle of it over a still background, moves on the scene plane during the exposure: `velocityMPerSec` `{ x?, y? }` in m/s (+x right, +y up) and an optional `angularVelocityRadPerSec` about the optical axis (counter-clockwise as seen from the camera; about the region's center for a region). Each scene query reads the subject at its own time, through the same temporal midpoints as camera rotation (`sensor.temporalSampleCount`, default 8 with a moving subject), so a rolling shutter skews a moving subject by its readout skew, and a camera yaw that follows the subject (panning) keeps it sharp and streaks the background. A region's rest rectangle shows a neutral 18 % background once the subject leaves it. It combines with `lens.diffraction`, `lens.defocus` (global shutter) and the lens options; `lens.pupilSampling` is refused. Off by default: requests without it are byte-identical.
|
|
141
|
+
- **Display view:** renders sideways motion (global shutter) through the lens signal's temporal midpoints; refuses `motion.subject.angularVelocityRadPerSec` and a rolling shutter with a reason (`simulateCapture` renders both).
|
|
142
|
+
- **Fast method** (registered, `motion.subject/temporal-midpoints`; `diagnostics.fast.applied["motion.subject"]`, assumption `fast.motion.subject`): with the default count and a still camera, fast takes one temporal midpoint per site of the subject's streak plus one when that is fewer than 8; exact keeps 8; an explicit `sensor.temporalSampleCount` always wins. `"motion.subject"` joins `SIMULATE_CAPTURE_FAST_OPTIONS` and `SIMULATE_CAPTURE_COST_OPTIONS`.
|
|
143
|
+
- **Root exports:** 1 new type (`SimulateCaptureSubjectMotion`).
|
|
144
|
+
|
|
145
|
+
### #550 Spatially varying illumination (mixed lighting)
|
|
146
|
+
|
|
147
|
+
- **New:** `simulateCapture` (and `simulateIdealReference`, `simulateDisplayView`, `prepareDisplayView`) option `illumination`: two to eight sources, each an illuminant at an illuminance, blended across the chart by a `linear-gradient`, a `radial` falloff or `regions` (docs/MIXED_ILLUMINATION.md). The irradiance at a point is Σ w_i·E_i(λ) with weights summing to 1 (exact for the declared blend; not a falloff model). It replaces `illuminant` and `illuminanceLux`. Root type exports: `MixedIllumination`, `MixedIlluminationBlend`, `MixedIlluminationPoint`, `MixedIlluminationPointSource`, `MixedIlluminationSource` (5 types, no runtime export).
|
|
148
|
+
- **Point sources (owner decision 2026-11-10, a step toward flash):** `illumination.points`, up to eight, each an illuminant, a position in metres in front of the plane and an intensity (`{ candela }` or `{ illuminanceLux, distanceM }`). Irradiance E = I·cos θ/d² (inverse square with Lambert's cosine law, from the CIE ILV definitions of luminous intensity and illuminance), added to the blend; exact for an ideal isotropic point, no occlusion. One source with points needs no blend; a point alone is allowed. Root type export `MixedIlluminationPointSource`.
|
|
149
|
+
- **Exposure and white balance:** scene-average metering reads the lit frame; the grey card sits at the frame center and reads the light there (`exposure.meteringCard`); `"scene-illuminant"` white balance uses the source strongest at the frame center (`output.whiteBalanceSource`); an explicit white balance wins. The assumption `scene.illumination` reports the blend.
|
|
150
|
+
- **Display view:** the CPU reference reads the radiance under the blend at every query point, as the capture does (no approximation, so no fast method). The GPU renderers refuse it with a reason (`unsupported-on-backend`, no device requested).
|
|
151
|
+
- **Capture:** a chart or material scene under a blend keeps the tabulated expected signal (#394), reading the radiance at each lattice point instead of per-material tables, verified per CFA phase against the chain (1e-9); caller reflectance functions take the chain.
|
|
152
|
+
- **Off by default:** requests without `illumination` keep their bytes. The module (`src/facade/mixed-illumination.ts`) was a lazy chunk on its branch; the integration made it a static import (size pass).
|
|
153
|
+
- **Size (rolldown 1.2.9, gzip level 9):** the app's capture-worker shape 355,994 → 358,671 B in total (+2,677 B: the lazily loaded `mixed-illumination` chunk 2,222 B with the point sources, the facade glue and the chunk split the rest), above its 356,000 B limit; per the owner, the integrator raises the limit by the exact remainder at integration. The capture-only entry chunk 263,817 → 264,706 B (+889 B); the display view's added entry size 18,187 → 16,197 B (the bundler moved shared modules between chunks).
|
|
154
|
+
|
|
155
|
+
### #580 Lateral color as a fringe width in pixels for a lens and sensor
|
|
156
|
+
|
|
157
|
+
- **New: `calculateLateralColorFringe({ lensId, activeImagingArea, pixelPitchMicrometers, fieldPosition? })`** (model `generic-lens-lateral-color-fringe` 1.0.0, for the app's Guide page on color fringing): a catalogue lens's blue-minus-red image displacement at a field position (`{ x, y }` in fractions of the half-width and half-height; default the top-right corner), radial and as x and y parts, in µm and in pixels, plus the fringe width |Δr| and the lens's F-to-C displacement. It reads the lens's own lateral-color model Δr(λ) = L·R·H·D(λ) at the lens corrections' channel wavelengths (610 and 465 nm), an approximation. Full frame, corner: PV 24 1.8 7.24 µm (1.21 px at 6 µm), PV 24 1.4 Pro 2.11 µm (0.35 px).
|
|
158
|
+
- **Tests** (`test/generic-lens-lateral-color.test.ts`): the formula written out from the Fraunhofer lines, linearity in the field height, direction in every quadrant, sensor scaling, every catalogue lens, refusals, and the chart measurements of the catalogue tests: at the corner region's centre it predicts 0.28 px (pro, measured 0.31 px) and 0.96 px (consumer, measured 0.71 px; the rendered edge also carries the consumer lens's larger blur, the demosaic and the color conversion).
|
|
159
|
+
- **Integration (2.21.0, owner decision):** an optional `channelDispersion` (#589's `calculateChannelEffectiveDispersion`) takes the camera's own channels; the result reports `channelDispersion` (and `channelWavelengthsNm: null` when it is given). Re-measured against #589's RAW measure at the corner region's centre (6 µm, default sensor and light): 0.232 px for *PV 24 1.4 Pro* (RAW 0.22 px) and 0.797 px for *PV 24 1.8* (RAW 0.80 px); the single-wavelength default gives 0.278 and 0.957 px.
|
|
160
|
+
- **Root exports:** 4 new (1 runtime): `calculateLateralColorFringe` and the types `CalculateLateralColorFringeInput`, `LateralColorFringe`, `LateralColorDisplacement`. A pure function in its own module (`src/optics/generic-lens-lateral-color.ts`); the capture worker does not import it.
|
|
161
|
+
- **Docs:** docs/LENS_CATALOGUE.md ("Lateral color as a fringe width"), docs/USAGE.md, API reference regenerated.
|
|
162
|
+
|
|
163
|
+
### #557 Representative light-source spectra designed by Photivra
|
|
164
|
+
|
|
165
|
+
- **New:** four representative light sources designed by Photivra (owner decision 2026-11-06: Photivra's own parametric spectra, no CIE data, no named products), each labeled "representative, not a named product": `tri-phosphor-fluorescent-4000k`, `white-led-low-rendering-4000k`, `white-led-high-rendering-4000k` and `warm-white-led-2700k`. Each is S(λ) = b + Σ aₖ·exp(−½((λ − μₖ)/σₖ)²) on 360–830 nm; formulas and design reasons in docs/LIGHT_SOURCES.md. `createRepresentativeLightSourceIlluminant({ sourceId })` returns `{ kind: "spectrum", spectrum, label }` for any `illuminant` option; `createRepresentativeLightSourceSpectrum({ sourceId })` the spectrum alone; `getRepresentativeLightSourceSet()` the frozen set.
|
|
166
|
+
- **Checked:** each spectrum's CCT with the engine's own locus search is within 1 K of its stated CCT (4000.8, 3999.6, 4000.0, 2699.9 K) with |Duv| ≤ 0.0001, and McCamy within 10 K. The same-CCT pair (low vs high color rendering, 4000 K) differs by ΔE00 5.05 on PV-S1, 4.67 on PV-P2 and 5.86 on PV-G1 (mean 3.68 over the 24 colored patches); against a Planckian reference at the same CCT the mean patch ΔE00 is 2.87, 4.00, 0.67 and 1.18. This is Photivra's own figure from its own patches, not CIE Ra or TM-30 (their data is not bundled).
|
|
167
|
+
- **Unchanged:** `illuminants.ts`, `scene-radiance.ts` and `simulateCapture` are untouched; the existing illuminants resolve byte-identically (hash locks of CIE A and Planckian 2700/6500 K, portable path). Use `sensor.spectralStepNanometers: 5` with these sources: at the default 20 nm the fluorescent's narrow bands leave a strong cast (documented and measured).
|
|
168
|
+
- **Root exports:** 11 new (7 runtime): `REPRESENTATIVE_LIGHT_SOURCE_SET_ID`, `REPRESENTATIVE_LIGHT_SOURCE_SET_VERSION`, `REPRESENTATIVE_LIGHT_SOURCE_IDS`, `REPRESENTATIVE_LIGHT_SOURCE_WAVELENGTH_RANGE_NANOMETERS`, `getRepresentativeLightSourceSet`, `createRepresentativeLightSourceSpectrum`, `createRepresentativeLightSourceIlluminant` and the types `RepresentativeLightSource`, `RepresentativeLightSourceBand`, `RepresentativeLightSourceId`, `RepresentativeLightSourceSet`.
|
|
169
|
+
- **Bundle:** the capture-worker shape is unchanged at 351,770 B gzip (limit 352,000 B); the sources reach only bundles that import them (+1,553 B gzip).
|
|
170
|
+
- **Docs and tests:** docs/LIGHT_SOURCES.md (new), docs/USAGE.md, docs/AGENT_GUIDE.md, llms.txt, THIRD_PARTY.md, API reference; `test/representative-light-sources.test.ts`.
|
|
171
|
+
|
|
172
|
+
### #365 Print planner: fit an image inside paper of another aspect ratio (contain), with the borders reported
|
|
173
|
+
|
|
174
|
+
- **New: `fit: { kind: "contain", paperOrientation: "as-given" | "match-image" }`** for `calculatePrintPlan` and `calculatePrintSizeLimit`: `printedImage` is then the paper; the image keeps the native aspect ratio, is scaled to the largest size inside the paper (scale `min(P_w/W, P_h/H)`, the "contain" sizing of CSS Images Level 3 §5.5) and centered, and is planned exactly as a `confirmed-native-aspect` plan of that size. `match-image` turns the paper a quarter turn to the image's orientation. The plan adds `contain: { paperMm, paperRotated, bordersMm: { left, right, top, bottom } }` (null without a native raster) and the size limit adds `maximumPaperMm`. Exact geometry (binary64 rounding reported in `relativeAspectError`); layout only, no crop or resampling. 6000 × 4000 on 8 × 10 in gives a 10 × 6.667 in image with 0.667 in borders on the long edges; 3:2 paper gives zero borders.
|
|
175
|
+
- **Unchanged:** `confirmed-native-aspect` plans and size limits are byte-identical to 2.19.0 (no new keys; checked over 720 inputs across every sampling kind, provider constraint, unit, orientation and unavailable source).
|
|
176
|
+
- **Type change:** `PrintPlanInput.fit` is now the union `PrintFit`; code that reads `fit.maximumRelativeAspectError` must first narrow on `fit.kind === "confirmed-native-aspect"`.
|
|
177
|
+
- **Root exports:** 2 new types, `PrintFit` and `PrintContainLayout`. Pure module (`src/output/print-plan.ts`); not in the capture worker.
|
|
178
|
+
- **Docs and tests:** docs/PRINT_PLANNING.md ("Contain fit"), docs/USAGE.md, API reference regenerated; `test/print-plan-contain.test.ts` (the issue's 8 × 10 in and 3:2 cases, as-given paper, portrait and square images, equality with the direct plan, size limit, unknown raster, input errors).
|
|
179
|
+
|
|
180
|
+
## 2.20.0 — published November 10, 2026
|
|
4
181
|
|
|
5
182
|
A minor release of ten issues and one speed-versus-accuracy scheme: adjustable sensor characteristics and the mid-gray noise figure (#555, #535), the aperture diameter (#577), the crop that matches a longer lens (#579), which limits detail, the lens or the sensor (#575), the print size limit with blur and noise (#574), the lens profile's vignetting area factor (#584), lens corrections as optional DNG opcodes (#587), each catalogue lens's own bokeh (#567), chart-boundary ties in the diffraction PSF quadrature (#565) and the CI Node drift check (#231). See the [2.20.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_20_0.md).
|
|
6
183
|
|
package/CITATION.cff
CHANGED
|
@@ -9,7 +9,7 @@ abstract: "An open TypeScript engine for the science and mathematics of photogra
|
|
|
9
9
|
repository-code: "https://github.com/photivra/photivra"
|
|
10
10
|
url: "https://photivra.com"
|
|
11
11
|
license: "Apache-2.0"
|
|
12
|
-
version: "2.
|
|
12
|
+
version: "2.21.0"
|
|
13
13
|
keywords:
|
|
14
14
|
- photography
|
|
15
15
|
- camera optics
|
package/README.md
CHANGED
|
@@ -32,10 +32,10 @@ The source is licensed under Apache-2.0.
|
|
|
32
32
|
|
|
33
33
|
## Quick start
|
|
34
34
|
|
|
35
|
-
Install the published ESM package below. The [2.
|
|
35
|
+
Install the published ESM package below. The [2.21.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_21_0.md) and [changelog](https://github.com/Photivra/photivra/blob/main/CHANGELOG.md) describe the current release:
|
|
36
36
|
|
|
37
37
|
```sh
|
|
38
|
-
npm install @photivra/engine@2.
|
|
38
|
+
npm install @photivra/engine@2.21.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
|
|
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
|
|
|
75
75
|
|
|
76
76
|
Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
|
|
77
77
|
|
|
78
|
-
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.
|
|
78
|
+
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.21.0` tag.
|
|
79
79
|
|
|
80
80
|
## Documentation
|
|
81
81
|
|
|
@@ -363,13 +363,14 @@ The application can then use those results, while the photography calculations t
|
|
|
363
363
|
|
|
364
364
|
## Status
|
|
365
365
|
|
|
366
|
-
- Repository package version: `2.
|
|
367
|
-
- Engine API contract: `2.
|
|
366
|
+
- Repository package version: `2.21.0` (release candidate prepared November 10, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.21.0`
|
|
368
368
|
- Versioned generic tier reference assets: [GENERIC_TIER_PRESETS.md](https://github.com/Photivra/photivra/blob/main/docs/GENERIC_TIER_PRESETS.md).
|
|
369
|
+
- Sensor profiles from published specifications (#554, unnamed): [SENSOR_PROFILES.md](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_PROFILES.md).
|
|
369
370
|
- Composed POC simulation API contract: `1.0.0`
|
|
370
371
|
- Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
|
|
371
372
|
|
|
372
|
-
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.
|
|
373
|
+
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.21.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
|
|
373
374
|
|
|
374
375
|
A breaking change to the root contracts now needs a new package major version. Additive and corrective changes follow [compatibility and migration](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_1_3_0.md). A 1.0 package is not a claim of calibrated physical accuracy.
|
|
375
376
|
|
package/SECURITY.md
CHANGED
|
@@ -4,7 +4,7 @@ Please do not open a public issue that contains vulnerability details.
|
|
|
4
4
|
|
|
5
5
|
## Supported versions
|
|
6
6
|
|
|
7
|
-
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.
|
|
7
|
+
The published 1.0.0 release started the 1.x root-package compatibility line, and 2.0.0 started the 2.x line. The current release is **2.20.0**, published on November 10, 2026 (see [CHANGELOG.md](CHANGELOG.md)).
|
|
8
8
|
|
|
9
9
|
Security fixes target the current maintained development and release line. Do not assume that older 1.x releases, snapshots or pre-1.0 releases will receive fixes.
|
|
10
10
|
|
package/THIRD_PARTY.md
CHANGED
|
@@ -44,6 +44,11 @@ These human reviews were **pending** until the owner signed all five off on 2026
|
|
|
44
44
|
| DNG/TIFF and JPEG sample provenance | All 26 tracked `.dng`/`.jpg` files name only "Photivra", "Photivra Virtual Camera" or "Photivra Print JPEG" (`strings` scan); no camera maker's name appears. The Adobe DNG notice is in `NOTICE`, `README.md` and the three DNG sources. **Not done:** byte-for-byte regeneration of the files. | **Signed off by the owner, 2026-11-09** (#544). |
|
|
45
45
|
| Transitive dependency/SBOM review | **Evidence complete for the 2.14.0 lockfile.** Recount of `package-lock.json`: 168 entries, with the licence counts given in [its summary](#per-release-transitive-dependency--sbom-review); no entry is a runtime dependency. `npm run check:licenses` passes. GitHub reports MIT for all four GitHub Actions in the table above (their repositories' detected licence). | **Signed off by the owner for 2.14.0–2.19.0, 2026-11-09** (#544). From 2.20.0 it is reviewed per release: each release's pull request gives the `npm run check:licenses` result and any new or changed licence against the previous release's lockfile. |
|
|
46
46
|
| Colorimetry published constants (`src/color/`). See [Colorimetry published constants and excluded datasets](#colorimetry-published-constants-and-excluded-datasets). | No CIE table is committed: the largest set of decimal constants in `src/color/` is 37, in `color-matching-functions.ts` (the engine's own fit). Each constant's source is cited in that section. On 2026-11-09 every constant was re-read against its free source (paywalled primaries through free secondaries, and numerically against the free CIE datasets), with no mismatch. | **Signed off by the owner, 2026-11-09** (#544). |
|
|
47
|
+
| Sensor profile sources (#554, `src/equipment/sensor-profiles.ts`). See [Sensor profile sources](#sensor-profile-sources-554). | **Signed off by the owner on 2026-11-09** (sources approved as listed; astronomy-camera manuals accepted for sensor values only; the retailer copy for one profile accepted, labelled; links allowed in `docs/SENSOR_PROFILES.md` only). Ten free sources and three standards (ISO 12232 labelled paywalled); every value is labelled published, derived or design. | Nothing for #554. |
|
|
48
|
+
|
|
49
|
+
## Sensor profile sources (#554)
|
|
50
|
+
|
|
51
|
+
`src/equipment/sensor-profiles.ts` holds eight sensor profiles whose values are facts taken from manufacturer-published specifications (sensor dimensions, pixel counts, pixel pitch, full well, read noise, peak QE, ADC bit depth, frame rate) and one openly published paper (International Image Sensor Workshop 2017, paper R03, Table 1), or Photivra design values labelled as such. They are factual references (`reuseStatus: "factual-reference-only"`); no table, curve, image or text is copied, and no maker or product is named in the code, data or docs. Commercial or terms-restricted measurement databases and test websites were excluded and not used (owner decision, 2026-11-06). ISO 12232:2019 is named for a cross-check only; no value depends on it. Sources, with links, and derivations: `docs/SENSOR_PROFILES.md`. The owner signed off the source review on 2026-11-09 (table above).
|
|
47
52
|
|
|
48
53
|
## Licence summaries (prepared for owner sign-off)
|
|
49
54
|
|
|
@@ -99,6 +104,8 @@ The colorimetry modules in `src/color/` (see [docs/COLORIMETRY.md](https://githu
|
|
|
99
104
|
| --- | --- | --- |
|
|
100
105
|
| 28 coefficients of the engine's own multi-lobe CIE 1931 CMF fit (`color-matching-functions.ts`) | Fitted by Photivra with `scripts/fit-cie-1931-cmf.mjs` to the CIE 1931 2° table, DOI 10.25039/CIE.DS.xvudnb9b (CIE, CC BY-SA 4.0). The model form, a sum of piecewise Gaussians, follows the idea in Wyman, Sloan & Shirley (2013), JCGT 2(2); none of that paper's coefficients or text are used. | Fit-time use only: the script downloads the table into a temporary directory outside the repository, checks its SHA-256 and deletes it. No CIE value is stored in the repository, the package, the tests or the script's output. The committed coefficients are our own least-squares result; 28 numbers cannot reproduce the 1,413-value table (maximum error 0.022). Attribution to the CIE dataset is given here and in `docs/COLORIMETRY.md`. Replaced the Wyman et al. coefficients by owner decision (2026-10-17). |
|
|
101
106
|
| Reference CIELAB and sRGB values of the engine's own color patches (`color-patches.ts`, `docs/COLOR_PATCHES.md`) | Computed by Photivra with `scripts/compute-color-patch-reference.mjs` from Photivra's own analytic spectra, using the CIE standard illuminant D65 table (DOI 10.25039/CIE.DS.hjfjmt59) and the CIE 1931 2° table (DOI 10.25039/CIE.DS.xvudnb9b), both CIE, CC BY-SA 4.0. The spectra themselves are Photivra's design; no ColorChecker, IT8 or other commercial target data, names, order or layout are used. | Computation-time use only, as for the CMF fit: the script downloads both tables into a temporary directory, checks their SHA-256 and deletes them. No CIE value is stored in the repository, the package, the tests or the script's output; the committed values (three CIELAB coordinates and one 8-bit sRGB colour per patch) are results computed from our spectra. Attribution to the CIE datasets is given here and in `docs/COLOR_PATCHES.md`. |
|
|
107
|
+
| Node spectra of the image-plane scene's print reflectance lattice (`image-plane-lattice-data.ts`, `docs/IMAGE_PLANE.md`, #581) | Fitted by Photivra with `scripts/fit-image-plane-lattice.mjs`: the LHTSS method of S. A. Burns, "Generating Reflectance Curves from sRGB Triplets", arXiv:1710.05732 v5 (2020), pp. 24–26 (the paper is CC BY-SA 4.0; its method is implemented from the equations, and neither its text nor its MATLAB code, also CC BY-SA 4.0, is used), against the CIE standard illuminant D65 table (DOI 10.25039/CIE.DS.hjfjmt59, https://files.cie.co.at/Publications-datasets/CIE_std_illum_D65.csv) and the CIE 1931 2° table (DOI 10.25039/CIE.DS.xvudnb9b, https://files.cie.co.at/Publications-datasets/CIE_xyz_1931_2deg.csv), both CIE, CC BY-SA 4.0. Owner decision 2026-11-09 (#581). | Fit-time use only, as for the CMF fit: the script downloads both tables into a temporary directory outside the repository, checks their SHA-256 and deletes them. No CIE value is stored in the repository, the package, the tests or the script's output. The committed data (120 spectra × 41 samples of 16 bits, delta-coded) are Photivra's own optimization results: each is the smoothest bounded spectrum that meets three integral constraints, and they cannot reproduce the tables. Attribution to the CIE datasets is given here and in `docs/IMAGE_PLANE.md`. |
|
|
108
|
+
| Representative light-source spectra (`representative-light-sources.ts`, `docs/LIGHT_SOURCES.md`, #557) | Photivra's own design (owner decision 2026-11-06): a constant plus Gaussian bands whose positions and widths Photivra chose for each kind of lamp, with two parameters per source solved by Photivra to put it on the Planckian locus at its stated CCT using the engine's own analytic CMF fit and Planck's law. No third-party spectral data is used: no CIE illuminant tables (the F-series fluorescent, LED-series or any other), no IES TM-30 or CIE test-sample reflectances, and no manufacturer or product spectra, names or data sheets. Their CCT and color-rendering figures are computed with the engine's own code and color patches only. | Photivra's own work under the project licence; nothing to attribute. Labeled "representative, not a named product". |
|
|
102
109
|
| Distinctness check of the color patches' published colors and names (`scripts/check-color-patch-distinctness.mjs`, `docs/COLOR_PATCHES.md`) | Reference colors compared against, each downloaded from a pinned URL and checked by SHA-256: the CSS named colors (W3C, CSS Color Module Level 4, CRD 2025-04-24; W3C Software and Document License, as linked from that document); the ColorChecker Classic's manufacturer sRGB values as tabulated in Wikipedia, "ColorChecker", revision 1367818471 (CC BY-SA 4.0); the ColorChecker's published X-Rite (before and after November 2014), GretagMacbeth 2005 and BabelColor average values as collected in colour-science 0.4.6 (BSD-3-Clause); RAL Classic approximate sRGB values as tabulated in Wikipedia, "List of RAL colours", revision 1369650571 (CC BY-SA 4.0); brand colors from Simple Icons 1.20.0, 2.19.0, 4.25.0, 9.21.0 and 16.36.0 (its data is dedicated to the public domain). Pantone data is proprietary and is not used. | Check-time use only: the script parses each source in memory, compares it with Photivra's own values and discards it. No third-party color value, name or table is stored in the repository, the package, the tests or the script's output, apart from what the script prints and `docs/COLOR_PATCHES.md` reports per patch as a factual result: the closest reference color's name, its set, its 8-bit sRGB hex (the "Its sRGB" column, one value per patch, 30 in all) and the ΔE00. The "Nudged patches" table there also names the reference colors the first design was too close to. `docs/COLOR_PATCHES.md` is repository documentation and is not in the npm package. The 30 hex values are attributed under its Result table: 7 ColorChecker and 6 RAL Classic values copied unchanged from the Wikipedia tables (CC BY-SA 4.0, credited with the title, authors, revision link and licence link that CC BY-SA 4.0 §3(a) asks for), 1 ColorChecker value converted to sRGB by the script from colour-science 0.4.6's CIELAB data (BSD-3-Clause, by the Colour Developers), 2 CSS named colors (W3C) and 14 Simple Icons brand colors (CC0 1.0). The patch names and colors are Photivra's own; the check only confirms that they differ. |
|
|
103
110
|
| Daylight-locus polynomial and M₁/M₂ coefficients; illuminant A formula constants; CIELAB, CIE94 and CIEDE2000 formula constants (`illuminants.ts`, `color-difference.ts`) | CIE 15:2018 (daylight and illuminant A constants; values checked against the free CIE datasets); CIELAB as in W3C CSS Color 4; CIE94 as stated in Wikipedia, "Color difference" (secondary source; the primary CIE 116-1995 is not free); Sharma, Wu & Dalal (2005), also CIE 142-2001 | Coefficients of standardized equations, used as factual constants. |
|
|
104
111
|
| McCamy CCT cubic | McCamy (1992), Color Res. Appl. 17(2) | Published equation coefficients. |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { prepareDevelopedLensCorrection, type DevelopedLensCorrectionSelection } from "../output/developed-lens-corrections.js";
|
|
1
|
+
import { prepareDevelopedLensCorrection, type ChannelDispersion, type DevelopedLensCorrectionSelection } from "../output/developed-lens-corrections.js";
|
|
2
2
|
/** @internal An opcode as written (application order), for the assumptions. */
|
|
3
3
|
export type DngLensCorrectionOpcode =
|
|
4
4
|
/** k0…k4 of g = 1 + k0r² + … + k4r¹⁰, and the largest |g/G − 1| on 2 048 radii in [0, 1]. */
|
|
@@ -26,7 +26,7 @@ export declare function createDngLensCorrectionOpcodes(lens: Parameters<typeof p
|
|
|
26
26
|
cornerStops: number;
|
|
27
27
|
};
|
|
28
28
|
};
|
|
29
|
-
}, selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number,
|
|
29
|
+
}, selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, channels: ChannelDispersion): {
|
|
30
30
|
opcodeList3: Uint8Array;
|
|
31
31
|
opcodes: DngLensCorrectionOpcode[];
|
|
32
32
|
};
|
|
@@ -40,7 +40,7 @@ export const DNG_VIGNETTE_FIT_TOLERANCE = 1e-3;
|
|
|
40
40
|
* @internal The OpcodeList3 bytes and the opcodes for a catalogue lens (`resolveGenericLens` at the capture's f-number, on the
|
|
41
41
|
* W × H ActiveArea of pitch `pitchMm`) and the selected corrections, at least one on (see the module notes).
|
|
42
42
|
*/
|
|
43
|
-
export function createDngLensCorrectionOpcodes(lens, selection, width, height, pitchMm,
|
|
43
|
+
export function createDngLensCorrectionOpcodes(lens, selection, width, height, pitchMm, channels) {
|
|
44
44
|
const opcodes = [], params = [];
|
|
45
45
|
// R_DNG / R: the stage-3 image's centre-to-corner distance over the profile's half-diagonal (1 when the frame is the lens's area).
|
|
46
46
|
const scale = Math.hypot(width, height) / 2 / (lens.halfDiagonalMm / pitchMm);
|
|
@@ -87,7 +87,7 @@ export function createDngLensCorrectionOpcodes(lens, selection, width, height, p
|
|
|
87
87
|
params.push([...best, 0.5, 0.5]);
|
|
88
88
|
}
|
|
89
89
|
if (selection.distortion || selection.lateralColor) {
|
|
90
|
-
const c = prepareDevelopedLensCorrection(lens, { ...selection, vignetting: false }, width, height, pitchMm,
|
|
90
|
+
const c = prepareDevelopedLensCorrection(lens, { ...selection, vignetting: false }, width, height, pitchMm, channels);
|
|
91
91
|
const planes = (selection.lateralColor ? c.scales : [1]).map((s) => { const a = c.fill * s; return [a, c.k1 * a ** 3 * scale * scale, 0, 0, 0, 0]; });
|
|
92
92
|
opcodes.push({ opcode: "WarpRectilinear", planes, magnification: 1 / c.fill });
|
|
93
93
|
params.push([planes.length, ...planes.flat(), 0.5, 0.5]);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dng-lens-correction-opcodes.js","sourceRoot":"","sources":["../../src/capture/dng-lens-correction-opcodes.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// This product includes DNG technology under license by Adobe.\n\n/**\n * @internal #587: a catalogue lens's generic corrections (`createGenericLensCorrectionProfile`, the same arithmetic as `output.lensCorrections`)\n * written as a DNG opcode list, so a RAW editor that applies DNG opcodes corrects the DNG non-destructively; the RAW codes are never\n * changed. Written from the Adobe DNG Specification (opcode lists were added in version 1.3.0.0; the repository's writer follows\n * 1.7.1.0), chapter \"Opcode List Processing\":\n *\n * - The list goes into OpcodeList3 (tag 51022, UNDEFINED), which a reader applies to the linear, demosaiced stage-3 image: the\n * ActiveArea, which for `simulateCapture` is exactly the output frame. The list is big-endian whatever the file's byte order: a\n * LONG opcode count, then per opcode its LONG id, the four-byte DNG version that introduced it (1.3.0.0), LONG flags and the LONG\n * byte count of its parameters. Every opcode is written with flag bit 0 (\"optional\") set, so a reader that does not support it may\n * skip it, and bit 1 (preview skip) clear.\n * - FixVignetteRadial (id 3) first, when vignetting is on: parameters k0…k4, cx, cy (DOUBLE); each pixel is multiplied by\n * g = 1 + k0r² + k1r⁴ + k2r⁶ + k3r⁸ + k4r¹⁰, r the distance from the optical centre (cx, cy) normalized to 1 at the image's\n * farthest corner. It runs before the warp, so it acts at the captured (source) position, where the capture applied the falloff.\n * The target gain is the profile's (#584): G(ρ) = det J(ρ_i)/E(ρ), E = (1 − bρ²)² the lens's relative illumination at the\n * capture's f-number and det J = (1 + k1ρ_i²)(1 + 3k1ρ_i²) at the ideal radius ρ_i with ρ_i(1 + k1ρ_i²) = ρ (the lens's own k1,\n * whether or not distortion is corrected). G is not a polynomial in r², so k0…k4 are a near-minimax fit of g/G − 1 (Lawson's\n * iteratively reweighted least squares) on 2 048 radii in [0, 1]; its largest |g/G − 1| there is reported, and a fit worse than\n * 0.1 % is refused (every catalogue lens fits within 0.07 %; GainMap, id 9, would be needed otherwise).\n * - WarpRectilinear (id 1) then, when distortion or lateral colour is on: N = 1 plane (no lateral colour) or 3 planes (red,\n * green, blue; CFAPlaneColor 0 1 2), each kr0…kr3, kt0, kt1, then cx, cy (DOUBLE). For a destination pixel at normalized\n * offset (dx, dy) from the centre, r² = dx² + dy², the source is the centre plus (dx, dy)·(kr0 + kr1r² + kr2r⁴ + kr3r⁶) plus the\n * tangential terms (kt0 = kt1 = 0 here). `output.lensCorrections` reads channel c at s = q(1 + k1|q|²/R²), q = m·a_c·p (R the\n * half-diagonal, a_c the channel's lateral-colour scale, m the zoom-to-fill factor): so kr0 = m·a_c, kr1 = k1·(m·a_c)³·(R_DNG/R)²,\n * kr2 = kr3 = 0 represent it exactly (R_DNG the centre-to-corner distance of the stage-3 image; equal to R for `simulateCapture`).\n * The zoom-to-fill of #570 is folded into kr0, so an editor's result has the corrected JPEG's framing.\n * - The optical centre is the image centre, cx = cy = 0.5 (0 the left or top edge, 1 the right or bottom edge).\n *\n * Limits: the gain is an approximation fitted to the profile (itself a labelled catalogue approximation); a reader's resampling\n * filter, and whether it applies opcodes at all, is the reader's. Some editors apply DNG opcodes automatically with no control in\n * the editor: the switch that is guaranteed is leaving a correction out of the DNG.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { prepareDevelopedLensCorrection, type DevelopedLensCorrectionSelection } from \"../output/developed-lens-corrections.js\";\n\n/** @internal An opcode as written (application order), for the assumptions. */\nexport type DngLensCorrectionOpcode =\n /** k0…k4 of g = 1 + k0r² + … + k4r¹⁰, and the largest |g/G − 1| on 2 048 radii in [0, 1]. */\n | { opcode: \"FixVignetteRadial\"; coefficients: number[]; maximumRelativeFitError: number }\n /** kr0…kr3, kt0, kt1 of one plane (all channels) or red, green, blue; the zoom-to-fill 1/m folded into kr0. */\n | { opcode: \"WarpRectilinear\"; planes: number[][]; magnification: number };\n\n/** @internal Largest accepted |g/G − 1| of the FixVignetteRadial fit. */\nexport const DNG_VIGNETTE_FIT_TOLERANCE = 1e-3;\n\n/**\n * @internal The OpcodeList3 bytes and the opcodes for a catalogue lens (`resolveGenericLens` at the capture's f-number, on the\n * W × H ActiveArea of pitch `pitchMm`) and the selected corrections, at least one on (see the module notes).\n */\nexport function createDngLensCorrectionOpcodes(lens: Parameters<typeof prepareDevelopedLensCorrection>[0] & { name: string; character: { vignetting: { cornerStops: number } } },\n selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, dispersion: (wavelengthNm: number) => number): { opcodeList3: Uint8Array; opcodes: DngLensCorrectionOpcode[] } {\n const opcodes: DngLensCorrectionOpcode[] = [], params: number[][] = [];\n // R_DNG / R: the stage-3 image's centre-to-corner distance over the profile's half-diagonal (1 when the frame is the lens's area).\n const scale = Math.hypot(width, height) / 2 / (lens.halfDiagonalMm / pitchMm);\n if (selection.vignetting) {\n const b = 1 - 2 ** (-lens.character.vignetting.cornerStops / 2), k1 = lens.character.distortion.corner;\n const target = (r: number): number => {\n const rho = r * scale, e = 1 - b * rho * rho;\n let ri = rho;\n for (let n = 0; n < 8; n++) ri -= (ri * (1 + k1 * ri * ri) - rho) / (1 + 3 * k1 * ri * ri);\n return (1 + k1 * ri * ri) * (1 + 3 * k1 * ri * ri) / (e * e);\n };\n // Near-minimax fit of the relative error g/G − 1 (t = r²: rows t^j/G, j = 1…5, against 1 − 1/G) by Lawson's iteratively\n // reweighted least squares on 2 048 radii in [0, 1] (normal equations, positive definite); of 64 iterations the one with the\n // smallest largest error on those radii is kept.\n const radii = Array.from({ length: 2048 }, (_, i) => { const r = i / 2047; return [r * r, target(r), 1]; });\n let best = [0, 0, 0, 0, 0], worst = Infinity;\n for (let iteration = 0; iteration < 64; iteration++) {\n const a = Array.from({ length: 5 }, () => [0, 0, 0, 0, 0, 0]), k = [0, 0, 0, 0, 0];\n for (const [t, G, w] of radii) {\n const row = [t!, t! ** 2, t! ** 3, t! ** 4, t! ** 5, G! - 1].map((v) => v / G!);\n for (let j = 0; j < 5; j++) for (let m = 0; m < 6; m++) a[j]![m]! += w! * row[j]! * row[m]!;\n }\n for (let j = 0; j < 5; j++) for (let i = j + 1; i < 5; i++) { const f = a[i]![j]! / a[j]![j]!; for (let m = j; m < 6; m++) a[i]![m]! -= f * a[j]![m]!; }\n for (let j = 4; j >= 0; j--) k[j] = (a[j]![5]! - a[j]!.slice(j + 1, 5).reduce((sum, v, m) => sum + v * k[j + 1 + m]!, 0)) / a[j]![j]!;\n const errors = radii.map(([t, G]) => Math.abs((1 + t! * (k[0]! + t! * (k[1]! + t! * (k[2]! + t! * (k[3]! + t! * k[4]!))))) / G! - 1)), max = Math.max(...errors);\n if (max < worst) { worst = max; best = k; }\n radii.forEach((p, i) => { p[2] = p[2]! * (errors[i]! / max + 1e-9); });\n }\n if (!(worst <= DNG_VIGNETTE_FIT_TOLERANCE)) throw new InvalidConfigurationError(\"output.dng.lensCorrectionOpcodes: FixVignetteRadial cannot hold \" + lens.name + \"'s vignetting gain within 0.1 %.\");\n opcodes.push({ opcode: \"FixVignetteRadial\", coefficients: best, maximumRelativeFitError: worst });\n params.push([...best, 0.5, 0.5]);\n }\n if (selection.distortion || selection.lateralColor) {\n const c = prepareDevelopedLensCorrection(lens, { ...selection, vignetting: false }, width, height, pitchMm, dispersion);\n const planes = (selection.lateralColor ? c.scales : [1]).map((s) => { const a = c.fill * s; return [a, c.k1 * a ** 3 * scale * scale, 0, 0, 0, 0]; });\n opcodes.push({ opcode: \"WarpRectilinear\", planes, magnification: 1 / c.fill });\n params.push([planes.length, ...planes.flat(), 0.5, 0.5]);\n }\n // Big-endian: the count, then per opcode its id, DNG version 1.3.0.0, flags 1 (optional), parameter bytes and parameters\n // (all DOUBLE but WarpRectilinear's leading LONG plane count).\n const size = (i: number): number => params[i]!.length * 8 - (opcodes[i]!.opcode === \"WarpRectilinear\" ? 4 : 0);\n const bytes = new Uint8Array(4 + params.reduce((sum, _, i) => sum + 16 + size(i), 0)), view = new DataView(bytes.buffer);\n view.setUint32(0, opcodes.length);\n let at = 4;\n params.forEach((values, i) => {\n const warp = opcodes[i]!.opcode === \"WarpRectilinear\";\n view.setUint32(at, warp ? 1 : 3); view.setUint32(at + 4, 0x01030000); view.setUint32(at + 8, 1); view.setUint32(at + 12, size(i)); at += 16;\n values.forEach((v, j) => { if (warp && j === 0) { view.setUint32(at, v); at += 4; } else { view.setFloat64(at, v); at += 8; } });\n });\n return { opcodeList3: bytes, opcodes };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dng-lens-correction-opcodes.js","sourceRoot":"","sources":["../../src/capture/dng-lens-correction-opcodes.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// This product includes DNG technology under license by Adobe.\n\n/**\n * @internal #587: a catalogue lens's generic corrections (`createGenericLensCorrectionProfile`, the same arithmetic as `output.lensCorrections`)\n * written as a DNG opcode list, so a RAW editor that applies DNG opcodes corrects the DNG non-destructively; the RAW codes are never\n * changed. Written from the Adobe DNG Specification (opcode lists were added in version 1.3.0.0; the repository's writer follows\n * 1.7.1.0), chapter \"Opcode List Processing\":\n *\n * - The list goes into OpcodeList3 (tag 51022, UNDEFINED), which a reader applies to the linear, demosaiced stage-3 image: the\n * ActiveArea, which for `simulateCapture` is exactly the output frame. The list is big-endian whatever the file's byte order: a\n * LONG opcode count, then per opcode its LONG id, the four-byte DNG version that introduced it (1.3.0.0), LONG flags and the LONG\n * byte count of its parameters. Every opcode is written with flag bit 0 (\"optional\") set, so a reader that does not support it may\n * skip it, and bit 1 (preview skip) clear.\n * - FixVignetteRadial (id 3) first, when vignetting is on: parameters k0…k4, cx, cy (DOUBLE); each pixel is multiplied by\n * g = 1 + k0r² + k1r⁴ + k2r⁶ + k3r⁸ + k4r¹⁰, r the distance from the optical centre (cx, cy) normalized to 1 at the image's\n * farthest corner. It runs before the warp, so it acts at the captured (source) position, where the capture applied the falloff.\n * The target gain is the profile's (#584): G(ρ) = det J(ρ_i)/E(ρ), E = (1 − bρ²)² the lens's relative illumination at the\n * capture's f-number and det J = (1 + k1ρ_i²)(1 + 3k1ρ_i²) at the ideal radius ρ_i with ρ_i(1 + k1ρ_i²) = ρ (the lens's own k1,\n * whether or not distortion is corrected). G is not a polynomial in r², so k0…k4 are a near-minimax fit of g/G − 1 (Lawson's\n * iteratively reweighted least squares) on 2 048 radii in [0, 1]; its largest |g/G − 1| there is reported, and a fit worse than\n * 0.1 % is refused (every catalogue lens fits within 0.07 %; GainMap, id 9, would be needed otherwise).\n * - WarpRectilinear (id 1) then, when distortion or lateral colour is on: N = 1 plane (no lateral colour) or 3 planes (red,\n * green, blue; CFAPlaneColor 0 1 2), each kr0…kr3, kt0, kt1, then cx, cy (DOUBLE). For a destination pixel at normalized\n * offset (dx, dy) from the centre, r² = dx² + dy², the source is the centre plus (dx, dy)·(kr0 + kr1r² + kr2r⁴ + kr3r⁶) plus the\n * tangential terms (kt0 = kt1 = 0 here). `output.lensCorrections` reads channel c at s = q(1 + k1|q|²/R²), q = m·a_c·p (R the\n * half-diagonal, a_c the channel's lateral-colour scale, m the zoom-to-fill factor): so kr0 = m·a_c, kr1 = k1·(m·a_c)³·(R_DNG/R)²,\n * kr2 = kr3 = 0 represent it exactly (R_DNG the centre-to-corner distance of the stage-3 image; equal to R for `simulateCapture`).\n * The zoom-to-fill of #570 is folded into kr0, so an editor's result has the corrected JPEG's framing.\n * - The optical centre is the image centre, cx = cy = 0.5 (0 the left or top edge, 1 the right or bottom edge).\n *\n * Limits: the gain is an approximation fitted to the profile (itself a labelled catalogue approximation); a reader's resampling\n * filter, and whether it applies opcodes at all, is the reader's. Some editors apply DNG opcodes automatically with no control in\n * the editor: the switch that is guaranteed is leaving a correction out of the DNG.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { prepareDevelopedLensCorrection, type ChannelDispersion, type DevelopedLensCorrectionSelection } from \"../output/developed-lens-corrections.js\";\n\n/** @internal An opcode as written (application order), for the assumptions. */\nexport type DngLensCorrectionOpcode =\n /** k0…k4 of g = 1 + k0r² + … + k4r¹⁰, and the largest |g/G − 1| on 2 048 radii in [0, 1]. */\n | { opcode: \"FixVignetteRadial\"; coefficients: number[]; maximumRelativeFitError: number }\n /** kr0…kr3, kt0, kt1 of one plane (all channels) or red, green, blue; the zoom-to-fill 1/m folded into kr0. */\n | { opcode: \"WarpRectilinear\"; planes: number[][]; magnification: number };\n\n/** @internal Largest accepted |g/G − 1| of the FixVignetteRadial fit. */\nexport const DNG_VIGNETTE_FIT_TOLERANCE = 1e-3;\n\n/**\n * @internal The OpcodeList3 bytes and the opcodes for a catalogue lens (`resolveGenericLens` at the capture's f-number, on the\n * W × H ActiveArea of pitch `pitchMm`) and the selected corrections, at least one on (see the module notes).\n */\nexport function createDngLensCorrectionOpcodes(lens: Parameters<typeof prepareDevelopedLensCorrection>[0] & { name: string; character: { vignetting: { cornerStops: number } } },\n selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, channels: ChannelDispersion): { opcodeList3: Uint8Array; opcodes: DngLensCorrectionOpcode[] } {\n const opcodes: DngLensCorrectionOpcode[] = [], params: number[][] = [];\n // R_DNG / R: the stage-3 image's centre-to-corner distance over the profile's half-diagonal (1 when the frame is the lens's area).\n const scale = Math.hypot(width, height) / 2 / (lens.halfDiagonalMm / pitchMm);\n if (selection.vignetting) {\n const b = 1 - 2 ** (-lens.character.vignetting.cornerStops / 2), k1 = lens.character.distortion.corner;\n const target = (r: number): number => {\n const rho = r * scale, e = 1 - b * rho * rho;\n let ri = rho;\n for (let n = 0; n < 8; n++) ri -= (ri * (1 + k1 * ri * ri) - rho) / (1 + 3 * k1 * ri * ri);\n return (1 + k1 * ri * ri) * (1 + 3 * k1 * ri * ri) / (e * e);\n };\n // Near-minimax fit of the relative error g/G − 1 (t = r²: rows t^j/G, j = 1…5, against 1 − 1/G) by Lawson's iteratively\n // reweighted least squares on 2 048 radii in [0, 1] (normal equations, positive definite); of 64 iterations the one with the\n // smallest largest error on those radii is kept.\n const radii = Array.from({ length: 2048 }, (_, i) => { const r = i / 2047; return [r * r, target(r), 1]; });\n let best = [0, 0, 0, 0, 0], worst = Infinity;\n for (let iteration = 0; iteration < 64; iteration++) {\n const a = Array.from({ length: 5 }, () => [0, 0, 0, 0, 0, 0]), k = [0, 0, 0, 0, 0];\n for (const [t, G, w] of radii) {\n const row = [t!, t! ** 2, t! ** 3, t! ** 4, t! ** 5, G! - 1].map((v) => v / G!);\n for (let j = 0; j < 5; j++) for (let m = 0; m < 6; m++) a[j]![m]! += w! * row[j]! * row[m]!;\n }\n for (let j = 0; j < 5; j++) for (let i = j + 1; i < 5; i++) { const f = a[i]![j]! / a[j]![j]!; for (let m = j; m < 6; m++) a[i]![m]! -= f * a[j]![m]!; }\n for (let j = 4; j >= 0; j--) k[j] = (a[j]![5]! - a[j]!.slice(j + 1, 5).reduce((sum, v, m) => sum + v * k[j + 1 + m]!, 0)) / a[j]![j]!;\n const errors = radii.map(([t, G]) => Math.abs((1 + t! * (k[0]! + t! * (k[1]! + t! * (k[2]! + t! * (k[3]! + t! * k[4]!))))) / G! - 1)), max = Math.max(...errors);\n if (max < worst) { worst = max; best = k; }\n radii.forEach((p, i) => { p[2] = p[2]! * (errors[i]! / max + 1e-9); });\n }\n if (!(worst <= DNG_VIGNETTE_FIT_TOLERANCE)) throw new InvalidConfigurationError(\"output.dng.lensCorrectionOpcodes: FixVignetteRadial cannot hold \" + lens.name + \"'s vignetting gain within 0.1 %.\");\n opcodes.push({ opcode: \"FixVignetteRadial\", coefficients: best, maximumRelativeFitError: worst });\n params.push([...best, 0.5, 0.5]);\n }\n if (selection.distortion || selection.lateralColor) {\n const c = prepareDevelopedLensCorrection(lens, { ...selection, vignetting: false }, width, height, pitchMm, channels);\n const planes = (selection.lateralColor ? c.scales : [1]).map((s) => { const a = c.fill * s; return [a, c.k1 * a ** 3 * scale * scale, 0, 0, 0, 0]; });\n opcodes.push({ opcode: \"WarpRectilinear\", planes, magnification: 1 / c.fill });\n params.push([planes.length, ...planes.flat(), 0.5, 0.5]);\n }\n // Big-endian: the count, then per opcode its id, DNG version 1.3.0.0, flags 1 (optional), parameter bytes and parameters\n // (all DOUBLE but WarpRectilinear's leading LONG plane count).\n const size = (i: number): number => params[i]!.length * 8 - (opcodes[i]!.opcode === \"WarpRectilinear\" ? 4 : 0);\n const bytes = new Uint8Array(4 + params.reduce((sum, _, i) => sum + 16 + size(i), 0)), view = new DataView(bytes.buffer);\n view.setUint32(0, opcodes.length);\n let at = 4;\n params.forEach((values, i) => {\n const warp = opcodes[i]!.opcode === \"WarpRectilinear\";\n view.setUint32(at, warp ? 1 : 3); view.setUint32(at + 4, 0x01030000); view.setUint32(at + 8, 1); view.setUint32(at + 12, size(i)); at += 16;\n values.forEach((v, j) => { if (warp && j === 0) { view.setUint32(at, v); at += 4; } else { view.setFloat64(at, v); at += 8; } });\n });\n return { opcodeList3: bytes, opcodes };\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal (#534): the sensor-stage hook of one render of an exposure build-up (`simulateExposureBuildUp`). After a site's
|
|
3
|
+
* ordinary photo and dark draws, `split` gets the render's expected means and those draws, and sets the counts the readout
|
|
4
|
+
* uses (`photo`, `dark`); `read` names the readout's read-noise stream. The build-up's own logic (binomial splitting, the
|
|
5
|
+
* per-site chain) lives in facade/exposure-build-up.ts, so captures without a build-up carry only this registry.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExposureBuildUpStage {
|
|
8
|
+
/** Sets `photo` and `dark` for the absolute native site `index` from this render's expected means and ordinary draws. */
|
|
9
|
+
split(index: number, photoMean: number, darkMean: number, photo: number, dark: number): void;
|
|
10
|
+
photo: number;
|
|
11
|
+
dark: number;
|
|
12
|
+
/** The read-noise stream ID of the readout (`deriveNativeSiteStreamSeedUint32`). */
|
|
13
|
+
read: number;
|
|
14
|
+
}
|
|
15
|
+
/** The running build-up renders' stages, by their native RAW input (`NativeRawInput`, the object the capture facade builds). */
|
|
16
|
+
export declare const EXPOSURE_BUILD_UP_STAGES: WeakMap<object, ExposureBuildUpStage>;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/** The running build-up renders' stages, by their native RAW input (`NativeRawInput`, the object the capture facade builds). */
|
|
3
|
+
export const EXPOSURE_BUILD_UP_STAGES = new WeakMap();
|
|
4
|
+
//# sourceMappingURL=exposure-build-up-stage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exposure-build-up-stage.js","sourceRoot":"","sources":["../../src/capture/exposure-build-up-stage.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAkBtC,gIAAgI;AAChI,MAAM,CAAC,MAAM,wBAAwB,GAAG,IAAI,OAAO,EAAgC,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (#534): the sensor-stage hook of one render of an exposure build-up (`simulateExposureBuildUp`). After a site's\n * ordinary photo and dark draws, `split` gets the render's expected means and those draws, and sets the counts the readout\n * uses (`photo`, `dark`); `read` names the readout's read-noise stream. The build-up's own logic (binomial splitting, the\n * per-site chain) lives in facade/exposure-build-up.ts, so captures without a build-up carry only this registry.\n */\n\nexport interface ExposureBuildUpStage {\n /** Sets `photo` and `dark` for the absolute native site `index` from this render's expected means and ordinary draws. */\n split(index: number, photoMean: number, darkMean: number, photo: number, dark: number): void;\n photo: number;\n dark: number;\n /** The read-noise stream ID of the readout (`deriveNativeSiteStreamSeedUint32`). */\n read: number;\n}\n\n/** The running build-up renders' stages, by their native RAW input (`NativeRawInput`, the object the capture facade builds). */\nexport const EXPOSURE_BUILD_UP_STAGES = new WeakMap<object, ExposureBuildUpStage>();\n"]}
|
|
@@ -41,6 +41,7 @@ import { parseFocusPlane } from "../optics/focus-state.js";
|
|
|
41
41
|
import { prepareCaptureExposureWindowSampler } from "../sensor/exposure-window.js";
|
|
42
42
|
import { calculateNativeRawSite, type NativeRawProducerFrameContext, type SensorRawProducerSiteInput } from "./sensor-raw-producer.js";
|
|
43
43
|
import type { NativeRawLeanOutput, NativeRawLeanSensorHook } from "./native-raw.js";
|
|
44
|
+
import type { ExposureBuildUpStage } from "./exposure-build-up-stage.js";
|
|
44
45
|
type Site = SimulateEnvironmentSensorRawFrameInput["sites"][number];
|
|
45
46
|
type Env = Site["environment"];
|
|
46
47
|
type FullPlan = ReturnType<typeof planEnvironmentSensorPhotoSignal>;
|
|
@@ -230,7 +231,7 @@ export interface NativeSiteReadout {
|
|
|
230
231
|
* its codes and its flags bit for bit. Its `compute` then gives `calculateNativeRawSite`'s outputs for any site of the same
|
|
231
232
|
* readout inputs (same CFA channel, dark current, capacity, readout regime and fixed-pattern profile).
|
|
232
233
|
*/
|
|
233
|
-
export declare function createVerifiedNativeSiteReadout(parsed: SensorRawProducerSiteInput, result: ReturnType<typeof calculateNativeRawSite>, whiteCode: number, photo: SensorEqeTemporalPhotoSignal, x: number, y: number, index: number, seed: number): NativeSiteReadout | undefined;
|
|
234
|
+
export declare function createVerifiedNativeSiteReadout(parsed: SensorRawProducerSiteInput, result: ReturnType<typeof calculateNativeRawSite>, whiteCode: number, photo: SensorEqeTemporalPhotoSignal, x: number, y: number, index: number, seed: number, b?: ExposureBuildUpStage): NativeSiteReadout | undefined;
|
|
234
235
|
/**
|
|
235
236
|
* Lean sensor stage for createNativeEnvironmentRawTask.
|
|
236
237
|
*
|