@photivra/engine 2.12.0 → 2.14.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 +144 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -7
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +3 -0
- package/dist/camera/lens-projection.js +2 -1
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/capture/dng-import.js +1 -1
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/export-jpeg.d.ts +1 -1
- package/dist/capture/export-jpeg.js +6 -4
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/host-yield-cadence.js +1 -1
- package/dist/capture/host-yield-cadence.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.js +1 -1
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-raw.js +1 -1
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/packed-sensor-raw.js +3 -3
- package/dist/capture/packed-sensor-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +1 -1
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/color/capture-color.js +3 -3
- package/dist/color/capture-color.js.map +1 -1
- package/dist/color/chromatic-adaptation.js +2 -2
- package/dist/color/chromatic-adaptation.js.map +1 -1
- package/dist/color/color-matching-functions.js +1 -1
- package/dist/color/color-matching-functions.js.map +1 -1
- package/dist/color/colorimetry-math.js +2 -2
- package/dist/color/colorimetry-math.js.map +1 -1
- package/dist/color/illuminants.js +3 -3
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/rgb-color-spaces.js +1 -1
- package/dist/color/rgb-color-spaces.js.map +1 -1
- package/dist/color/sensor-color-development.js +1 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/core/record-validation.js +3 -3
- package/dist/core/record-validation.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-prime-lenses.js +3 -3
- package/dist/equipment/generic-prime-lenses.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +23 -1
- package/dist/equipment/generic-tier-assets.js +58 -30
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.d.ts +6 -3
- package/dist/equipment/generic-tier-presets.js +26 -13
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
- package/dist/exposure/auto-iso-slowest-shutter.js +217 -0
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
- package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
- package/dist/exposure/exposure-mode-resolver.js +238 -33
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -1
- package/dist/exposure/flash.js.map +1 -1
- package/dist/facade/chart-material-layers.js +2 -2
- package/dist/facade/chart-material-layers.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +1 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +45 -8
- package/dist/facade/chart-scenes.js +101 -44
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/color-patches.d.ts +129 -0
- package/dist/facade/color-patches.js +209 -0
- package/dist/facade/color-patches.js.map +1 -0
- package/dist/facade/diffraction-provider.js +12 -13
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu.js +1 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view.d.ts +27 -0
- package/dist/facade/display-view.js +234 -65
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/scene-radiance.js +2 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +139 -12
- package/dist/facade/simulate-capture.js +209 -36
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +2 -2
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +2 -2
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.js +1 -1
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
- package/dist/gpu/display-view-quality-gpu.js +24 -14
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/temporal-sum-gpu.js +23 -23
- package/dist/gpu/webgpu-runtime.js +1 -1
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
- package/dist/gpu/wgsl-chart-kernel.js +117 -117
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.js +522 -488
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-kernels.js +203 -203
- package/dist/gpu/wgsl-kernels.js.map +1 -1
- package/dist/gpu/wgsl-sensor-kernel.js +302 -302
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/motion/temporal-exposure-sum.js +1 -1
- package/dist/motion/temporal-exposure-sum.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.js +4 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/front-of-lens-filter.js +1 -1
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +10 -0
- package/dist/optics/irradiance-convolution.js +49 -14
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/output/native-capture-sdr.js +1 -1
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/png-encoder.js +2 -2
- package/dist/output/png-encoder.js.map +1 -1
- package/dist/output/print-jpeg.js +13 -10
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/output/text-metadata.d.ts +22 -2
- package/dist/output/text-metadata.js +58 -9
- package/dist/output/text-metadata.js.map +1 -1
- package/dist/sensor/fixed-pattern-noise.js +1 -1
- package/dist/sensor/fixed-pattern-noise.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -1
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/docs/API_REFERENCE.md +49 -1
- package/docs/QUICK_START.md +104 -9
- package/docs/USAGE.md +241 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-tier-assets.md +41 -1
- package/docs/api/equipment-generic-tier-presets.md +11 -3
- package/docs/api/exports.json +172 -2
- package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
- package/docs/api/exposure-exposure-mode-resolver.md +68 -0
- package/docs/api/facade-chart-scenes.md +36 -5
- package/docs/api/facade-color-patches.md +150 -0
- package/docs/api/facade-simulate-capture.md +140 -9
- package/docs/api/output-text-metadata.md +34 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,149 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.14.0 — unreleased (prepared November 1, 2026)
|
|
4
|
+
|
|
5
|
+
A minor release for the app's Beta: color test charts from the engine's own spectra (#537, with Photivra names, codes and reasons, #542), grey-card (incident) exposure (#538), third-stop ISO and shutter grids (#541) and a smaller package (#540). Captures, RAW codes, files and assumptions are the same as 2.13.0 for every request that uses none of the new patches, `exposure.metering: "grey-card"`, third-stop ISOs or `exposure.autoIso.isoSteps`.
|
|
6
|
+
- **New:** built-in Photivra color patches: the `color-patches` chart preset, the chart reflectance `{ kind: "color-patch", patchId }`, `getColorPatchSet()`, `evaluateColorPatchReflectance()`, each patch with a short code, a name and a reason (#537, #542). `exposure.greyCardDeviationStops` and `exposure.metering: "scene-average" | "grey-card"` (#538). Third-stop ISO settings on the generic tier bodies (presets 2.1.0), `exposure.autoIso.isoSteps`, and `GENERIC_THIRD_STOP_SHUTTER_SECONDS` (#541).
|
|
7
|
+
- **Changed:** the published shader text is minified and module-level frozen tables are `/* @__PURE__ */`, so the app's bundle is 6.6 KiB gzip smaller with the same GPU results (#540).
|
|
8
|
+
- **Compatibility:** 21 new root exports (1,801; runtime exports 600 → 611), none removed or changed:
|
|
9
|
+
- color patches, 14: 6 runtime exports (`COLOR_PATCH_SET_ID`, `COLOR_PATCH_SET_VERSION`, `COLOR_PATCH_IDS`, `COLOR_PATCH_WAVELENGTH_RANGE_NANOMETERS`, `getColorPatchSet`, `evaluateColorPatchReflectance`) and 8 types (`ColorPatch`, `ColorPatchId`, `ColorPatchGroup`, `ColorPatchSet`, `ColorPatchSpectralTerm`, `EvaluateColorPatchReflectanceInput`, `ColorPatchReflectance`, `ColorPatchesChartScene`);
|
|
10
|
+
- grey card, 2: `SIMULATE_CAPTURE_METERING_MODES` and the type `SimulateCaptureMetering`;
|
|
11
|
+
- third-stop grids, 5: `GENERIC_THIRD_STOP_ISO_SETTINGS`, `GENERIC_THIRD_STOP_SHUTTER_SECONDS`, `GENERIC_WHOLE_STOP_ISO_SETTINGS`, `GENERIC_EQUIPMENT_TIER_PRESET_VERSIONS` and the type `GenericTierPresetVersion`.
|
|
12
|
+
- `ChartReflectance` and `ChartElementScene` gain a member; `CHART_SCENE_PRESETS` and `CHART_SCENE_DEFAULTS` gain `color-patches`; the capture's and the display view's `exposure` gain `greyCardDeviationStops` and `metering`; the Auto ISO report gains `isoSteps` when it is given.
|
|
13
|
+
- `GENERIC_EQUIPMENT_TIER_PRESET_VERSION` is now `"2.1.0"`; `"2.0.0"` still resolves, unchanged. Captures by tier id report preset and profile version 2.1.0, and their request records list a readout regime per third-stop ISO.
|
|
14
|
+
- No default changed: the meter stays `"scene-average"`, Auto ISO stays in whole stops, and a scalar reflectance is a flat grey as before.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- **Color patches designed by Photivra (docs/COLOR_PATCHES.md).** 30 reflectance spectra in six groups: a six-step flat neutral ramp from white (90.1 %) to black (3.5 %); red, yellow, green, cyan, blue and magenta, each strong and soft; four skin tones; sky blue, leaf green, brick red, sand, teal and lilac; a deep orange and a deep violet near the sRGB gamut edge. Each has a stable id, a stable short code (`PV-N1`…`PV-N6`, `PV-S1`…, `PV-T1`…, `PV-P1`…`PV-P4`, `PV-M1`…, `PV-G1`, `PV-G2`), an original Photivra name in US English ("Studio Gray 4", "Darkroom Red", "Midday Sky") and one sentence on why it is on the chart, all published in a table in docs/COLOR_PATCHES.md.
|
|
19
|
+
- **Distinct from third-party colors (owner request, 2026-11-01):** no published hex equals, and no colored patch is within ΔE00 2.0 (CIEDE2000, D65) of, a CSS named color, a published ColorChecker Classic sRGB or CIELAB value, a RAL Classic approximate sRGB value or one of 10,386 brand colors from Simple Icons; neutrals, flat by definition, only avoid equal hexes (they stay within ΔE00 0.35 of the ColorChecker's neutral ramp). `scripts/check-color-patch-distinctness.mjs` downloads the pinned sources, checks their SHA-256 and discards them; no third-party color data is committed. Thirteen colors of the first design were nudged and their spectra refit: five neutrals by 0.001–0.004 in reflectance and eight colors by ΔE00 0.56–4.04 (the strong red 3.53 and the deep orange 4.04 most); Pantone was not checked (proprietary). The names do not reuse ColorChecker, CSS or RAL names.
|
|
20
|
+
- **Spectra:** R(λ) = min(1, max(0, b + Σ aₖφₖ(λ))) on 360–830 nm, with Gaussian bumps exp(−½((λ − μ)/σ)²) and logistic steps 1/(1 + exp(−(λ − λ₀)/s)). Photivra fitted the parameters once to its own target colors and rounded them; the rounded values are the definition and are published in full. Every spectrum lies in [0.03, 0.901], so the clamp never acts; no fluorescence. No ColorChecker, IT8 or other commercial target's patch data, names, order, layout or measured spectra are used.
|
|
21
|
+
- **API:** `{ kind: "color-patch", patchId }` is accepted wherever a chart takes a reflectance spectrum (`uniform`, `patches`, material scenes and material layers). The `color-patches` preset draws a list of patches (default: all 30, six per row) as `patches` does, with `columns`, `gapFraction` and `backgroundReflectance`; its parameters name the set and its version, and its spectra enter the scene digest. `getColorPatchSet()` returns the frozen set (ids, codes, names, groups, reasons, parameters, reference values); `evaluateColorPatchReflectance()` evaluates one spectrum (approximation, model `photivra-color-patch-spectrum` 1.0.0). Invalid ids, fields and values fail with `InvalidConfigurationError`, wavelengths outside 360–830 nm with `InvalidScientificInputError`.
|
|
22
|
+
- **Reference colorimetry:** each patch's CIELAB under CIE D65 with the CIE 1931 2° observer (CIE tables, 1 nm, 360–830 nm) and its 8-bit sRGB. All 30 are inside sRGB; the gamut-edge patches' smallest linear components are 0.0105 and 0.0133. `scripts/compute-color-patch-reference.mjs` recomputes them from the CIE tables, which it downloads, checks by SHA-256 and deletes (nothing from them is stored), and with `--render` renders the chart under D65: maximum ΔE00 0.19 at 1 nm and 0.24 at 5 nm against the published values.
|
|
23
|
+
- **Display view:** it evaluates the patches of chart presets with the portable exponential, so those values have the same bits in every JavaScript engine; captures use `Math.exp`.
|
|
24
|
+
- **Cost:** a 600 × 500 capture of the chart costs the same as a neutral 30-patch chart (95–101 ms against 93–95 ms, one machine). The patch table adds about 9,800 B minified (about 4,100 B gzip, 3,285 B and 1,420 B of it the codes, names and reasons) to a capture-only bundle; the display view's addition is unchanged.
|
|
25
|
+
- **Tests:** `test/color-patches.test.ts`:
|
|
26
|
+
- the set contract, frozen records and the documented groups; codes numbered per group, names in one style and in US English, one-sentence reasons, no ColorChecker patch names;
|
|
27
|
+
- the formula against the published terms on a 0.25 nm grid, and the unused clamp;
|
|
28
|
+
- the neutral L* against 116·R^(1/3) − 16, the published hex against the published CIELAB, and the published hue order and skin lightness order;
|
|
29
|
+
- 35 invalid reflectances, charts and inputs;
|
|
30
|
+
- `color-patches` renders exactly what `patches` with the same reflectances renders, and a material scene what a uniform chart renders;
|
|
31
|
+
- neutral after white balance under the 6500 K Planckian and illuminant A (ideal reference and a noisy capture);
|
|
32
|
+
- hue order red, yellow, green, cyan, blue, magenta for the strong and the soft patches, lower chroma for the soft ones, skin tones by lightness;
|
|
33
|
+
- CIELAB rendered under the 6500 K Planckian within ΔE00 1.35 of the published D65 values (measured 1.12, mean 0.39);
|
|
34
|
+
- the display view;
|
|
35
|
+
- bit-identical renders of existing charts against digests recorded with 2.13.0.
|
|
36
|
+
- **Incident (grey-card) exposure deviation.** `exposure.greyCardDeviationStops` = log₂(L_ref(N, t, S)/(0.18·E/π)), with L_ref = 10·N²/(0.65·t·S) the luminance that the settings expose at the CIPA DC-004-2020 standard output sensitivity reference exposure H = 10/S (§3.2; camera equation §5.5 (4), the same constants as `standardExposureDeviationStops` and `SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX`), and 0.18·E/π the luminance of an 18 % Lambertian grey card under the request's illuminance E. Positive is darker (under), as for the metered deviation. It reads the light falling on the scene, as a grey card or a hand-held incident meter does, so it does not depend on the scene's reflectances: a white chart, which makes the scene meter read bright, leaves it unchanged. `null` for a `radiance-function` scene (its radiance ignores the illuminance).
|
|
37
|
+
- **Exposure modes unchanged:** aperture priority, shutter priority and program still meter the scene with the body's zone meter; the new field only reports where their exposure sits against the grey card (docs/QUICK_START.md, "Exposure"). On the app's round 4 All-in-one chart (90 % white paper) at the default illuminance they expose 1.78 stops darker than the grey card.
|
|
38
|
+
- **Tests:** `test/exposure-grey-card.test.ts`:
|
|
39
|
+
- exactly 0 at the defaults, in the capture and the display view;
|
|
40
|
+
- one stop per stop of shutter, aperture and ISO and per doubling of the illuminance (to 1e-12 stops), and the closed form at marked settings;
|
|
41
|
+
- the same value for four scenes of different reflectances and for a fixed-size chart with a dark surround, while the metered deviation differs;
|
|
42
|
+
- in aperture priority on a 90 % white field it exceeds the metered deviation by log₂(0.9/0.18);
|
|
43
|
+
- shutter priority and program with Auto ISO follow the closed form of the settings used;
|
|
44
|
+
- null for a radiance scene, 0 for a black scene at the defaults.
|
|
45
|
+
|
|
46
|
+
- **Grey-card metering (`exposure.metering`).** `"scene-average"` (default) keeps the body's 16 × 16-zone scene meter. `"grey-card"` gives every meter zone the luminance 0.18·E/π of an 18 % Lambertian grey card under the request's illuminance, so aperture priority, shutter priority and program, with a fixed ISO or Auto ISO, choose settings with `greyCardDeviationStops` = 0 whatever the scene's reflectances; only the mode's own steps and limits remain (whole-stop Auto ISO: at most half a stop, equal to the Auto ISO report's `residualStops`; the lens's f/4 or the shutter range when the light is beyond them). The result reports `exposure.metering`; with `"grey-card"` an assumption `exposure.meteringSource` gives the card's luminance. A `radiance-function` scene is refused with grey-card metering. Unknown values fail with `InvalidConfigurationError`. The scene-luminance fields keep describing the scene.
|
|
47
|
+
- **Tests** (`test/exposure-grey-card.test.ts`): the default is scene-average and an explicit `"scene-average"` gives the same bytes, settings and assumptions; unknown values and a radiance scene are refused; each automatic mode lands within 1e-6 stops of 0 at two illuminances, and compensation moves it by exactly the compensation; settings do not depend on the scene's reflectances; with Auto ISO in all four modes the deviation equals the report's `residualStops` (to 1e-9), within half a stop for `iso-grid-quantization`, and an `iso-maximum` limit is reported as such; the display view and the ideal reference resolve the same settings.
|
|
48
|
+
|
|
49
|
+
Images, RAW codes, every other exposure field, the assumptions and the scene digests are identical to 2.13.0 for manual, aperture-priority, program Auto ISO and shutter-priority captures and a display view (compared by SHA-256 between the two builds; every hash lock holds).
|
|
50
|
+
|
|
51
|
+
- **Third-stop exposure grids (owner's round 5, item 8).** The app's dials step in thirds.
|
|
52
|
+
- **ISO:** the generic tier presets 2.1.0 (`GENERIC_EQUIPMENT_TIER_PRESET_VERSION`) take third-stop ISO settings, `GENERIC_THIRD_STOP_ISO_SETTINGS` = 100, 125, 160, 200 … 25 600 (the rounded values of 100·2^(k/3), each within 2 %), up to each body's maximum (Consumer 6400, Prosumer 12 800, Professional 25 600), each with its own readout regime from the same gain model. Preset 2.0.0 is kept exactly (manifest pin unchanged) and still resolves; `GENERIC_EQUIPMENT_TIER_PRESET_VERSIONS` = `["2.0.0", "2.1.0"]`.
|
|
53
|
+
- **Auto ISO keeps whole stops** by default, so its choices are those of 2.13; `exposure.autoIso.isoSteps: "third-stop"` lets it choose every setting (reported as `autoIso.isoSteps` when given). Auto ISO limits must lie on the grid it uses.
|
|
54
|
+
- **Shutter:** `GENERIC_THIRD_STOP_SHUTTER_SECONDS`, the standard third-stop times from 1/8000 s to 30 s (55 values, the rounded values of 2^(k/3) s, each within 7 %). The bodies' shutters stay continuous and take every value as the exact time.
|
|
55
|
+
- **Same output:** whole-stop requests on the tier ids give the same samples, RAW codes, files, settings and assumptions as the 2.0.0 records (`test/third-stop-settings.test.ts`, five requests on each tier), and the 22-result fingerprint is identical in every typed array. The result fields that change are the version identifiers (`provenance.cameraPresetVersion` and `exposure.resolution.capabilityProfiles.{body,lens}ProfileVersion` read 2.1.0) and the request records (`request.environmentRaw`, `request.siteInput(i)`), whose readout profile now lists a regime per third-stop ISO; `test/simulate-capture-integration.test.ts` re-pins those six request digests and none of its sample, code, flag, development or scene digests.
|
|
56
|
+
- **Tests:** `test/third-stop-settings.test.ts`: the series against 100·2^(k/3) and 2^(k/3) s; both catalogs pinned; selections of both versions; ISO 125–320 expose in proportion and are refused on a 2.0.0 record; whole-stop byte identity; Auto ISO whole-stop by default and thirds on request (300 lx: 6400 → 8000); limits on the grid; every shutter time accepted as given.
|
|
57
|
+
|
|
58
|
+
### Changed
|
|
59
|
+
|
|
60
|
+
- **Smaller published package (#536).** The app was 2 KiB under its 500 KiB JavaScript budget.
|
|
61
|
+
- **Shader text:** `npm run build` strips comments and indentation from the `/* wgsl */` shader literals (`scripts/minify-wgsl-dist.mjs`, `scripts/lib/wgsl-minify.mjs`), keeping every newline and every line that holds an interpolation, so the WGSL token stream, line numbers and the engine's own shader-text anchors are unchanged; the build fails if any of that changes. Tests and the WebGPU suites run the minified text (`PHOTIVRA_WGSL_MINIFY=0` for the source). docs/WEBGPU.md, "Shipped shader text".
|
|
62
|
+
- **Pure annotations:** 110 module-level `Object.freeze(<literal>)` table constructors are `/* @__PURE__ */`, so bundlers drop unused tables; in-function freezes are untouched.
|
|
63
|
+
- **Measured:** on the app's production build (0.50.0, Vite 8), the GPU session chunk 52,917 → 46,430 B gzip and the app's total JavaScript 500.7 → 494.1 KiB gzip; importing only `SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX` 1,175 → 457 B gzip.
|
|
64
|
+
- **Same output:** on an Apple M5 Pro, `npm run test:webgpu` passed all 167 tests with the minified and with the source shader text, with identical recorded values (2,911); the CPU suite and every hash lock pass with the minified text. `test/wgsl-minify.test.ts` checks every literal's tokens with an independent tokenizer, the 14 shader-text anchors, and the edge cases.
|
|
65
|
+
|
|
66
|
+
### Known
|
|
67
|
+
|
|
68
|
+
- At the default `sensor.spectralStepNanometers` (20 nm) the facade's scene-illuminant white balance is measured on 1 nm nodes while the sensor integrates on 20 nm nodes, which leaves a small neutral cast that depends on the illuminant's spectral detail: white a* 0.17, b* 0.36 under the default 6500 K Planckian, b* −0.17 under illuminant A, and a* −0.74, b* +0.95 under the CIE D65 table as a caller spectrum. With 5 nm nodes it is below 0.03. Unchanged in this release (changing it would change existing captures); docs/COLOR_PATCHES.md recommends 5 nm for color work.
|
|
69
|
+
|
|
70
|
+
## 2.13.0 — published October 31, 2026
|
|
71
|
+
|
|
72
|
+
A minor release for the app's Beta camera: Auto ISO with photographer-set limits and an automatic slowest shutter, the illuminance that gives the standard exposure, and root exports for adding text metadata to a file the caller encoded. Captures, RAW codes and files are the same as 2.12.0 for the same request; Quality (AHD) display views change where the scene decides AHD's choice, closer to the capture (#457).
|
|
73
|
+
- **New:** `estimateAutoIsoSlowestShutter()` and `exposure.autoIso.slowestPreferredShutterSeconds: "auto"` (#504); `autoIsoLimits` on every Auto ISO resolver and `exposure.autoIso.limits` (#505); `exposure.standardExposureIlluminanceLux`; `addJpegTextMetadata` and `addPngTextMetadata` (#388).
|
|
74
|
+
- **Fixed:** the Quality (AHD) display view follows AHD's direction choice (#457).
|
|
75
|
+
- **Faster, same output:** diffraction captures (#419); the print JPEG task (#417).
|
|
76
|
+
- **Compatibility:** 13 new root exports (1,780), none removed or changed; new optional options and result fields only; no default changed. `ExposureResolutionConstraint` gains two members, `"user-iso-maximum"` and `"user-iso-minimum"`, reported only when the new limits are given.
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- **Auto ISO: an automatic slowest shutter from the lens and the hand's shake, with a faster/slower bias (#504).** New `estimateAutoIsoSlowestShutter()` (approximation, model `camera-shake-blur-budget-slowest-shutter` 1.0.0). It gives the exposure time at which `estimateCameraShakeBlur()`'s stabilized on-axis blur, v·hypot(tan(r·ω_yaw·t), tan(r·ω_pitch·t)) with r = 2^−stops, equals a blur budget (in pixels of a pitch, or a fraction of the image diagonal). Then it applies a whole-stop bias `fasterByStops` (−2 to +2: derived · 2^−bias) and, when `shutterCapability` is given, snaps to the body's shutter grid without ever lengthening the time. One axis uses the closed form t = atan(budget/v)/(r·|ω|); two axes use deterministic bisection. The 1/focal-length reciprocal rule is a rule of thumb and is not used. Makers' manuals for the feature: Sony ISO AUTO Min. SS, Fujifilm ISO Auto setting, Nikon D850 Auto ISO control.
|
|
81
|
+
- **Hand shake, a labelled generic default:** `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC` = 4.39 °/s (0.0766 rad/s) on yaw. It is the linear camera-motion rate for an average user holding a 100 g handset with two hands, in F. Xiao, J. Farrell, P. Catrysse, B. Wandell, "Mobile imaging: the big challenge of the small pixel", Proc. SPIE 7250 (2009), §2 (free author copy at web.stanford.edu/~wandell). It comes from the gyroscope measurements of Xiao et al., Proc. SPIE 6502 (2007). It describes a phone, not any camera or photographer, so the estimator takes the profile as an explicit input; the default is only the facade's.
|
|
82
|
+
- **Program + Auto ISO:** `resolveProgramAutoExposureMode` takes an optional `isoControl.slowestPreferredShutter: { slowestPreferredShutterSeconds, afterMaximumIso }` (`ProgramSlowestPreferredShutterPolicy`). The line's shutter is held at or faster than the threshold, with the line's aperture, before the ISO rises. At the Auto ISO maximum (the body's or #505's `autoIsoLimits`), `allow-slower-shutter` lengthens the shutter toward the target but never past the line's own shutter, and `hold-preferred-shutter` keeps it. The result adds `slowestPreferredShutter: { …, applied, afterMaximumIsoFallbackUsed }` only when the option is given. Full Auto does not take it.
|
|
83
|
+
- **`simulateCapture` and the display view:** `exposure.autoIso.slowestPreferredShutterSeconds` accepts `"auto"` in aperture priority and is now optional in program (a number or `"auto"`, with `afterMaximumIso`). `exposure.autoIso.automaticSlowestShutter` sets `fasterByStops` (default 0), `blurBudgetPixels` (default 1), `angularVelocityRadPerSec` (default the generic hand) and `stabilizationStopsEquivalent` (default 0); each default is a `default` assumption, and the derivation is a `model` assumption. The Auto ISO report gains `automaticSlowestShutter` (derived, biased and used times, budget, blur at the used time in pixels) and, in program, `slowestPreferredShutterApplied`.
|
|
84
|
+
- **Unchanged without the options:** every hash lock holds. A numeric slowest shutter in aperture priority and program without the option give the same results and reports as before.
|
|
85
|
+
- **Tests:** `test/auto-iso-slowest-shutter.test.ts`:
|
|
86
|
+
- the closed form, and the shake model's blur at the derived time equals the budget (one and two axes);
|
|
87
|
+
- nine focal lengths give strictly faster thresholds, with 400 mm 8× faster than 50 mm;
|
|
88
|
+
- the thin-lens projection at a focus distance;
|
|
89
|
+
- stabilization lengthens by exactly 2^stops;
|
|
90
|
+
- the bias moves by exact whole stops;
|
|
91
|
+
- the diagonal-fraction budget;
|
|
92
|
+
- the discrete-grid snap and its clamps;
|
|
93
|
+
- no NaN or Infinity across a sweep;
|
|
94
|
+
- 15 invalid inputs fail with domain errors.
|
|
95
|
+
|
|
96
|
+
`test/auto-iso-limits.test.ts` adds the program hold: applied and not applied, both after-maximum policies, the fallback stopping at the line's shutter, and invalid holds. `test/simulate-capture-auto-iso.test.ts` adds the facade:
|
|
97
|
+
- aperture priority with `"auto"`;
|
|
98
|
+
- equality with a direct estimator call;
|
|
99
|
+
- focal length and bias;
|
|
100
|
+
- program with a number and with `"auto"`;
|
|
101
|
+
- the display view report;
|
|
102
|
+
- 11 invalid options fail.
|
|
103
|
+
- New root exports (1,780): `estimateAutoIsoSlowestShutter`, `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`, `AUTO_ISO_SLOWEST_SHUTTER_MODEL_VERSION`, `AUTO_ISO_SLOWEST_SHUTTER_MAXIMUM_BIAS_STOPS`, and the types `EstimateAutoIsoSlowestShutterInput`, `AutoIsoSlowestShutterBlurBudget`, `AutoIsoSlowestShutterEstimate`, `ProgramSlowestPreferredShutterPolicy`, `SimulateCaptureAutomaticSlowestShutterOptions` and `SimulateCaptureAutomaticSlowestShutterReport`. Docs: `docs/USAGE.md` ("Automatic slowest shutter (#504)", "Program Auto with Auto ISO: a slowest preferred shutter (#504)"), `docs/QUICK_START.md` ("Auto ISO") and `docs/STABILIZATION.md`.
|
|
104
|
+
- **Auto ISO: photographer-set limits, separate from the body's capability (#505).** A new optional policy input `autoIsoLimits: { maximum, minimum? }` (type `AutoIsoLimits`) on every Auto ISO entry point: `resolveManualExposureMode` (`isoControl.autoIsoLimits`), `resolveAperturePriorityAutoIsoExposureMode` (`policy.autoIsoLimits`), `resolveShutterPriorityExposureMode` (`isoControl.policy.autoIsoLimits`), `resolveProgramAutoExposureMode` (`isoControl.autoIsoLimits`) and `resolveFullAutoExposureMode` (`policy.autoIsoLimits`); and `exposure.autoIso.limits` in `simulateCapture`, the display view and the ideal reference, in every mode. Sources, makers' manuals: Nikon D850 Auto ISO control (maximum sensitivity), Fujifilm X-Pro2 ISO Auto setting (default and maximum sensitivity), Panasonic GH5 II (upper and lower Auto ISO limits).
|
|
105
|
+
- **Policy, not capability:** the body's `capabilities.iso.autoIsoMinimum`/`autoIsoMaximum` (else its ISO range) still say what the body allows; the limits narrow that range. Both must be selectable ISOs inside it, `minimum` at most `maximum` (default the body's minimum); otherwise the resolvers fail with `InvalidScientificInputError` (a malformed record with `InvalidConfigurationError`) and `simulateCapture` with `InvalidConfigurationError`. Nothing is clipped. The policies' lowest-selectable-ISO baseline starts at the user's minimum.
|
|
106
|
+
- **Reporting:** `ExposureResolutionConstraint` gains `"user-iso-maximum"` and `"user-iso-minimum"`, reported when the ISO is held at a limit strictly inside the body's range (a limit equal to the body's bound reports the body's `"iso-maximum"`/`"iso-minimum"`). `SimulateCaptureAutoIsoReport` gains the optional `limits: { minimum, maximum }`, the range used; `autoIsoMinimum`/`autoIsoMaximum` stay the body's range. The `exposure.autoIso` assumption records the limits when given.
|
|
107
|
+
- **Unchanged without the input:** no result, sample or hash changes when `autoIsoLimits` / `exposure.autoIso.limits` is absent; every hash lock holds. `exposure.autoIso` is now accepted with `iso: "auto"` in every mode, but `slowestPreferredShutterSeconds` and `afterMaximumIso` stay aperture priority only. A separate flash cap is not included (it belongs with flash-aware exposure).
|
|
108
|
+
- **Tests:** `test/auto-iso-limits.test.ts` (each of the five resolvers stops at the user maximum with `user-iso-maximum` and the exact residual; a maximum equal to the body's reports `iso-maximum` and changes nothing; the user minimum raises the baseline and reports `user-iso-minimum`; aperture priority's `allow-slower-shutter` and `hold-preferred-shutter` give the same settings and residual against a user cap of 800 as against a body capped at 800; limits equal to the body's range give results equal to no limits; out-of-range, off-grid, unordered, non-finite, missing and malformed limits fail in every resolver) and `test/simulate-capture-auto-iso.test.ts` (the facade in manual, aperture priority, shutter priority and program; a capped capture byte-identical to that manual ISO; the display view and ideal reference report the same limits; ten invalid options fail).
|
|
109
|
+
- New root export: the type `AutoIsoLimits` (1,770 root symbols). Docs: `docs/USAGE.md` ("Photographer-set Auto ISO limits (#505)", "Priority modes with Auto ISO") and `docs/QUICK_START.md` ("Auto ISO").
|
|
110
|
+
- **The illuminance at which the settings give the standard exposure.** `simulateCapture`'s and the display view's `exposure` gain `standardExposureIlluminanceLux`, next to `standardExposureDeviationStops`: E·L_ref(N, t, S)/L = E·2^Δ, with E the request's `illuminanceLux`, L the metered scene luminance and L_ref = 10·N²/(0.65·t·S) (CIPA DC-004-2020, as for Δ). A chart, reflectance-function or material scene's radiance is reflectance × k(E)·S(λ)/π with k proportional to E, so its metered luminance is proportional to E and the same request at this illuminance meters at Δ = 0. It is `null` for a black scene (L = 0) and for a `radiance-function` scene, whose radiance does not follow the illuminance; it may exceed the 1e7 lux that `illuminanceLux` accepts. No sample, RAW code or other result changes; every hash lock holds.
|
|
111
|
+
- Tests (`test/exposure-standard-illuminance.test.ts`): re-running at the reported illuminance gives |Δ| < 1e-9 stops (≈ 2e-15 measured; the 16 × 16-zone, 5 nm metering quadrature is the same at both illuminances and cancels) for the default request at f/11 (40 612 lux) and a 3200 K Siemens star at 5000 lux (10 557 lux); with the defaults it is the default illuminance to 1e-4 (the quadrature's ≈ 1.2 × 10⁻⁵ stop); aperture priority equals manual at the settings it resolved, shutter priority and program are E·2^Δ, the display view equals the capture; `null` for a black and a radiance scene. `test/simulate-capture-auto-iso.test.ts` expects the new field in the display view's exposure.
|
|
112
|
+
- **Display-view bundle size, measured and guarded (#383).** Tests and documentation only; no `src/` change, so no result, sample or RAW code changes and every hash lock holds. Sizes with rolldown 1.2.9 (Vite 8's bundler), minified, gzip level 9, from 2.11.1's `dist`, in docs/DISPLAY_VIEW.md, "Bundle size":
|
|
113
|
+
- **The display view adds 42,228 B minified and 16,888 B gzip** to a `simulateCapture` + `simulateIdealReference` bundle (846,928 B / 229,007 B gzip). #383's target of 40 % less is not met: the addition is mostly the renderer itself (55 % of the rendered size), the result's assumption texts and the portable functions, and no smaller form was found that keeps every result's bytes and the measured render times. `test/capture-bundle.test.ts` keeps the addition within a budget about 2 % above the measured size: 43,100 B minified and 17,250 B gzip on 2.11.1's display view, raised to 46,500 B and 18,550 B for #457's AHD choice (#527), which added 3,307 B minified and 1,294 B gzip (the test's build from `src/` measures 45,546 B / 18,186 B gzip; all of the growth is `facade/display-view.ts`, rendered 55,908 → 64,855 B, with no new import and no `src/gpu` module in the entry).
|
|
114
|
+
- **The GPU display view stays out of a split bundle's first load:** a bundle of `simulateCapture`, `simulateDisplayView` and `prepareDisplayView` has no `src/gpu` module in its entry chunk (892,566 B / 247,193 B gzip); the GPU session, the renderers and their WGSL (15 `src/gpu` modules, 52,446 B gzip) are a chunk `prepareDisplayView` imports only for `gpu: "auto"` or `"require"`. The new test fails if they reach the entry. A build that cannot split inlines them (302,217 B gzip as one `iife` file): Vite's default `iife` worker format is how the GPU code reached the Photivra app's capture worker in 2.11; with `worker: { format: "es" }` its worker measured 258,618 B gzip instead of 317,120 B. docs/USAGE.md notes this under "Rendering a prepared view on the GPU".
|
|
115
|
+
- **`addJpegTextMetadata` and `addPngTextMetadata` are root exports (#388).** A caller that encodes a JPEG or PNG itself (for example a full-size file joined from `output.region` parts with `composePrintJpegStrips` or `serializePngImage`) adds the caller's text with the functions `output.metadata` already calls, so the bytes are the same as a whole-frame `simulateCapture` with `output.metadata`. This lets Photivra/apps delete its adapted copy (`src/export/textMetadata.ts`). No file, sample or RAW code changes; every hash lock holds.
|
|
116
|
+
- **Validation:** the bytes must be a `Uint8Array`; the text is checked by `output.metadata`'s rules (messages name `addJpegTextMetadata metadata` or `addPngTextMetadata metadata`). The JPEG writer now also rejects, with `InvalidConfigurationError`, a marker without a length before SOS (SOI, EOI, RSTm, TEM, fill bytes), a segment length below 2, an XMP APP1 segment after a non-APPn segment (before, it would have duplicated the bytes in between), and an XMP packet that is not UTF-8 (before, a raw `TypeError`). The engine's own JPEGs have none of these.
|
|
117
|
+
- **Tests** (`test/output-text-metadata.test.ts`): for three files (8-bit JPEG, 8-bit PNG, 16-bit Display P3 PNG) and four texts (the app's export text, the app encoder test's `{ description: "x" }`, and this file's Latin-1 and non-Latin vectors), the exported functions give the bytes of `simulateCapture` with `output.metadata`, locked by SHA-256 digests that were recorded three ways and agreed: the app's copy run unmodified on the same inputs, `output.metadata` at origin/main `50dfe3e`, and the exports; the same for a JPEG without an XMP packet (against the app's copy) and the ideal-reference PNG; plus sixteen invalid inputs.
|
|
118
|
+
- **Naming:** `add*` is a new verb family in `docs/API_STYLE.md` (a copy of an encoded file with caller data inserted, every other byte kept), with `test/api-style-compatibility.test.ts` updated; the two functions are classified in `test/root-export-input-errors.test.ts` (raw bytes and the text).
|
|
119
|
+
- New root exports: `addJpegTextMetadata` and `addPngTextMetadata` (1,769). Docs: `docs/USAGE.md` ("Text metadata on a file you encoded"), `docs/QUICK_START.md` ("Text metadata (2.4)"); API reference regenerated.
|
|
120
|
+
|
|
121
|
+
### Changed
|
|
122
|
+
|
|
123
|
+
- **Tests and CI: three hash locks are x64 locks, with recorded arm64 hashes (#508).** No engine output, package content or x64 hash lock changes. The display view's "keeps its bytes" locks, the pixel-memo locks and the Airy cell construction locks were recorded on x64; on arm64 (Apple silicon and Linux arm64, Node 24.19.0 and 24.22.1) 13 of their 17 digests differ, because V8's `Math` `sin`, `cos`, `asin`, `atan2` and `log` return results one ulp apart on the two architectures for some arguments (first a `Math.cos` in the β = 12.5 Airy cells and `Math.log(14)` in the display view; neither architecture is the more accurate). On x64 the tests assert the locks exactly as before; on arm64 (`process.arch === "arm64"`) each differing lock is asserted against its own recorded arm64 hash (`test/helpers/architecture-hash-locks.ts`), so arm64 is locked too, and the whole suite passes on Apple silicon. The display-view test no longer claims "the same on every JavaScript engine". The `hash locks (macos-latest)` job's step for these three files no longer has `continue-on-error`, so a failure there fails the step instead of showing as passed. Docs: `docs/NUMERICAL_CORRECTNESS.md` ("Test hash locks and platforms", which also corrects the macOS job to eight test files) and `docs/DISPLAY_VIEW.md` ("Same bytes in every engine").
|
|
124
|
+
|
|
125
|
+
### Fixed
|
|
126
|
+
|
|
127
|
+
- **Display view, AHD (Quality): the expected value follows AHD's choice where the scene decides it (#457).** The view averaged AHD's two directional candidates at every real pixel. Where structure finer than a few sites decides AHD's homogeneity choice (a Siemens star's centre, a sharp edge), the capture takes one candidate, so at device-pixel widths the view's colour at the star's centre was 5.1 codes RMS and up to 34 codes from the capture (24 MP at 2028 wide; limit 2). The view now runs AHD's own steps on the noise-free mosaic (the expected sites, clipped at white, reflect-101 at the raster edges, as `createAhdDemosaicTask`) and takes the chosen candidate's expected value; a tie, which is every flat neighbourhood, keeps the average, and the noise terms are unchanged. docs/DISPLAY_VIEW.md, "AHD's choice where the scene decides it".
|
|
128
|
+
- **Measured** (seven reference seeds, limits unchanged): set F star centre 5.13 → 0.78 codes RMS (24 MP, 2028 wide) and 3.60 → 1.02 (100 MP, 4000 wide); AHD alone 19.3 → 2.84 against the seven-reference mean, where one reference is 3.08 from it. On the default matrix 50 of 75 Quality cases change and none leaves a limit; star-centre RMS falls (for example 1.64 → 0.82 codes, 24 MP at 800 wide), case 17's false-colour difference +0.41 → +0.01 and case 92's +0.48 → +0.41. No Speed case and no uniform patch changes, so the Quality noise limit (#379) is unchanged.
|
|
129
|
+
- **The view's direction is the task's:** equal to `createAhdDemosaicTask`'s direction map at every pixel of a random and a star mosaic, borders included (`test/display-view-ahd-choice.test.ts`, with the set F star case against seven captures as a regression test).
|
|
130
|
+
- **Bytes:** Quality (AHD) display views change wherever the two candidates differ; Speed views and the views of uniform scenes keep their bytes. Three hash locks were re-recorded on Linux x64, each closer to the capture against 16 reference captures: `test/display-view.test.ts` (AHD dead leaves, Reinhard: 0.37 → 0.16 codes RMS), `test/display-view-pixel-memo.test.ts` (AHD star: 1.51 → 0.61) and `test/display-view-speed-material.test.ts` (AHD patch edge at ISO 3200: 7.56 → 3.88; its `sceneLookups` diagnostic rises 26 400 → 28 252 because the choice reads 5 sites around each pixel). The other locks hold. Captures are unchanged.
|
|
131
|
+
- **Cost:** +12–15 % on the app's Quality request at 2028 × 1352 (whole view extrapolated 23.4–24.4 s → 26.9–28.0 s, Node 24.19.0, Apple M5 Pro), +12–21 % for whole Quality views on the stratified estimator.
|
|
132
|
+
- **Bundle:** the display view's addition to a capture bundle grows by 3,307 B minified and 1,294 B gzip (42,239 → 45,546 B, 16,888 → 18,186 B gzip; rolldown 1.2.9, `test/capture-bundle.test.ts`'s build), all in `facade/display-view.ts`; #383's budget is raised for it.
|
|
133
|
+
- **GPU (Quality):** the kernel still renders the candidates' average; it now flags (`ahdChoice`) and the host recomputes with the CPU reference every display pixel where the choice could move it by more than 2⁻¹⁰ of its noise σ, recomputing runs of flagged pixels as one region. GPU renders keep agreeing with the CPU reference within their bounds, but busy scenes recompute a large share on the CPU (3–75 % of the emulation fixtures' pixels; 21.6 % of a 24 MP checkerboard band at 2028 × 1352). Computing the choice in the pixel pass is the follow-up. docs/WEBGPU.md, "Display view (Quality) on the GPU".
|
|
134
|
+
|
|
135
|
+
### Faster, same output
|
|
136
|
+
|
|
137
|
+
- **`simulateCapture` with `lens.diffraction`: the object-space convolution computes only each band's own values (#419).**
|
|
138
|
+
- **What changed:** each 32-row band convolved its whole padded grid, (rows + 2R) × (columns + 2R), and kept the centre (about 3× the rows at R = 32, and more on narrow rasters). It now gathers only the interior (internal `createIrradianceConvolutionInteriorRunner`), with the same validation and messages as before. The scene samples per band are unchanged. Defocus (#492) already did this.
|
|
139
|
+
- **Same bytes:** each interior value is the same products added in the same order (v outer, u inner, every tap), so it is `calculateIrradianceConvolution`'s value bit for bit. Checked with `Object.is` on 2,354,304 provider values against main's provider (R 1–32, material and radiance-function scenes, three offset sets, four windows, three bands), on new unit tests of the interior runner, and on twelve captures whose samples, RAW codes and `providerEvaluationCount` are identical (SHA-256) to main's. Every hash lock holds.
|
|
140
|
+
- **Measured** (Node 24.19.0, one thread, an 18-core Mac shared with other jobs, load 6–43, five rounds alternating fresh processes of the two builds; medians): `diagnostics.timingsMs.diffraction` on a function scene with the Quality options (diffraction, four-spot, AHD) at 96 × 72 went 0.86 → 0.16 s at 1.4 µm f/8 (R 15), 4.9 → 0.54 s at f/16 (R 29) and 6.3 → 0.62 s at 1 µm f/16 (R 32), total 7.2 → 1.8 s; at 192 × 144 and R 32, 17.0 → 2.1 s (total 19.6 → 5.2 s). The tabulated capture's four verification sites (a 2 × 2 window) went 0.41 → 0.004 s at 1.4 µm f/16 (total 0.85 → 0.50 s at 96 × 72 and 1.05 → 0.76 s at 384 × 288). At small R nothing slows down: 6 µm f/8 and the 24 MP whole-frame view are within ±5 % over eight rounds.
|
|
141
|
+
- **Not done:** a row ring sharing the 2R overlap samples between bands. Sampling is about 6 % of the band time at R 29–32 after this change (measured), so the ring would save at most about 4 % of it, for up to 2R rows × columns × nodes × lattices of doubles (about 42 MB per lattice on a 4000-site row).
|
|
142
|
+
- **Print JPEG task: each block's samples in one reused `Uint8Array`, without the per-sample check (#417).** `createPrintJpegTask` built every 8 × 8 block's 192 samples as a new `number[]` by `push`, and the encoder then checked each entry with `Number.isInteger` and the 0–255 bounds. Both paths (1:1 and area average) now write the samples into one reused `Uint8Array(192)`, and the internal encoder's `writeRgbBlock` skips the entry scan for a `Uint8Array`, whose entries are integers 0–255 by type; it still checks the length. The strip path (`createPrintJpegStrip`, the app's workers) already coded from a `Uint8Array` and is unchanged. #416 (2.8.1) had already replaced the 1:1 accumulation with a 256-entry table; the DCT and entropy coding are untouched.
|
|
143
|
+
- **Same bytes:** the files' SHA-256 equals main's (50dfe3e) for nine plans: 6000 × 4000 noise and smooth, 3000 × 2000 noise, 1001 × 757 noise, 9 × 7, 256 × 32, 1543 × 1 at 1:1, and 3000 × 2000 → 1500 × 1000 and 1001 × 757 → 270 × 205 area averages. Every JPEG hash lock holds. New test: a reused `Uint8Array` block codes to the same bytes as arrays, and a 191- or 193-sample one is refused.
|
|
144
|
+
- **Measured** (Node 24.19.0, one thread, Apple M5 Pro, with six other agents running on the machine, so absolute times are noisy; builds alternated, 3 rounds × 3 runs, median of 9): 24 MP noise at 1:1, q = 2: 3.06 → 2.90 s (×0.95); 24 MP smooth at 1:1, q = 1: 1.46 → 1.32 s (×0.91); 6 MP → 1.5 MP area average: 0.363 → 0.349 s (×0.96). The saving is below #417's profiled ceiling of about 27 %.
|
|
145
|
+
|
|
146
|
+
## 2.12.0 — published October 31, 2026
|
|
4
147
|
|
|
5
148
|
A minor release for the app's Beta camera: Auto ISO through the one-call API, and the deviation from the standard exposure for an exposure meter. With `iso` absent or a number, samples, RAW codes and files are the same as 2.11.1; the exposure result gains `isoControl` and `standardExposureDeviationStops`.
|
|
6
149
|
- **New:** `exposure.iso: "auto"` in every exposure mode, with `exposure.autoIso` in aperture priority and an Auto ISO report in the result (#516, #518); `exposure.standardExposureDeviationStops` (#515, #517).
|
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.14.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.14.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_14_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.14.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
|
|
@@ -57,7 +57,7 @@ console.log(capture.assumptions.map((a) => `${a.id}: ${a.explanation}`));
|
|
|
57
57
|
|
|
58
58
|
`simulateCapture()` runs the full physical pipeline through the engine's existing contracts: scene radiance, lens, spectral sensor, noise, RAW readout, RAW development and optional JPEG. It lists every default and modelling choice it made in `assumptions`.
|
|
59
59
|
|
|
60
|
-
The [Quick Start](https://github.com/Photivra/photivra/blob/main/docs/QUICK_START.md) covers the inputs, the chart presets (slanted edge, Siemens star, step wedge, grey ramp, checkerboard, point grid, caller patches), illuminants, white balance and limits.
|
|
60
|
+
The [Quick Start](https://github.com/Photivra/photivra/blob/main/docs/QUICK_START.md) covers the inputs, the chart presets (slanted edge, Siemens star, step wedge, grey ramp, checkerboard, point grid, caller patches and, from 2.14, Photivra's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md)), illuminants, white balance and limits.
|
|
61
61
|
|
|
62
62
|
The individual calculations are still available as small functions. For example, the horizontal field of view for a 36 mm sensor dimension and a 50 mm focal length:
|
|
63
63
|
|
|
@@ -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.14.0` tag.
|
|
79
79
|
|
|
80
80
|
## Documentation
|
|
81
81
|
|
|
@@ -363,13 +363,13 @@ 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.14.0` (release candidate prepared November 1, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.14.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
369
|
- Composed POC simulation API contract: `1.0.0`
|
|
370
370
|
- Stability: 1.0 root-package compatibility policy; bounded scientific models and independent POC contract
|
|
371
371
|
|
|
372
|
-
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.
|
|
372
|
+
From 1.0.1, the package version and `ENGINE_API_VERSION` are equal. New captures/plans record 2.14.0; existing archives keep their original creator identities. Schema, model and POC contract versions stay independent.
|
|
373
373
|
|
|
374
374
|
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
375
|
|
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.13.0**, published on October 31, 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
|
@@ -92,11 +92,14 @@ The colorimetry modules in `src/color/` (see [docs/COLORIMETRY.md](https://githu
|
|
|
92
92
|
| Constants | Source | Licence / basis for use |
|
|
93
93
|
| --- | --- | --- |
|
|
94
94
|
| 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). |
|
|
95
|
+
| 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`. |
|
|
96
|
+
| 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 Document License); 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 the closest reference color's name, set and ΔE00 that the script prints and `docs/COLOR_PATCHES.md` reports per patch as a factual result. The patch names and colors are Photivra's own; the check only confirms that they differ. |
|
|
95
97
|
| 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. |
|
|
96
98
|
| McCamy CCT cubic | McCamy (1992), Color Res. Appl. 17(2) | Published equation coefficients. |
|
|
97
99
|
| Bradford and CAT16 matrices (`chromatic-adaptation.ts`) | Lam (1985) / ICC.1:2010 Annex E; CAT16: Li et al. (2016), IS&T CIC24, Eq. (15), doi:10.2352/ISSN.2169-2629.2017.32.208 (free; checked 2026-10-17 to match the engine; journal version Li et al. 2017) | Published 3×3 model constants. CAT02, the Hunt–Pointer–Estévez matrix and the CIECAM02 degree-of-adaptation formula were removed from 2.0 (owner decisions, 2026-10-17) because no confirmed free source prints them. |
|
|
98
100
|
| Primaries and white points; BT.709 and BT.1886 constants (`rgb-color-spaces.ts`) | ITU-R BT.709-6, BT.1886, BT.2020-2; W3C CSS Color 4 (`srgb`, `display-p3`); also IEC 61966-2-1, SMPTE EG 432-1 | Standardized numeric constants; every value is available from the freely published ITU-R and W3C documents. |
|
|
99
101
|
| h, c, k | BIPM SI Brochure (2019) | Exact SI defining constants (facts). |
|
|
102
|
+
| Generic handheld angular velocity 4.39 °/s (`GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`, `src/exposure/auto-iso-slowest-shutter.ts`, #504) | F. Xiao, J. Farrell, P. Catrysse, B. Wandell, "Mobile imaging: the big challenge of the small pixel", Proc. SPIE 7250 (2009), §2 (free author copy: https://web.stanford.edu/~wandell/data/papers/2009-CameraShake-Xiao.pdf, read 2026-10-31); from the gyroscope measurements of F. Xiao et al., Proc. SPIE 6502 (2007) | One published measured value used as a fact, labelled as a generic default for a 100 g handset held in two hands, not a measurement of any camera or photographer. No text, figure, table or dataset is copied. |
|
|
100
103
|
| Standard output sensitivity S = 10/H, camera-equation constant 0.65 and the 18 % grey reference, K_m = 683 lm/W (`src/facade/`) | CIPA DC-004-2020 (free); BIPM SI Brochure, definition of the candela | Standardized scalar constants used as facts. The capture facade's charts (slanted edge, Siemens star, step wedge, grey ramp, checkerboard, point grid) are engine-authored analytic patterns; the slanted edge only follows the ISO 12233 geometry (a few degrees of slant, 4:1 contrast) and reproduces no chart artwork or data. |
|
|
101
104
|
|
|
102
105
|
The following datasets are deliberately **excluded**:
|
|
@@ -33,7 +33,8 @@ import { calculatedResult } from "../core/calculation-result.js";
|
|
|
33
33
|
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
34
34
|
import { rejectUnknownFields, requireInputKeys } from "../core/record-validation.js";
|
|
35
35
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
36
|
-
export const LENS_PROJECTION_MODELS =
|
|
36
|
+
export const LENS_PROJECTION_MODELS =
|
|
37
|
+
/* @__PURE__ */ Object.freeze([
|
|
37
38
|
"rectilinear",
|
|
38
39
|
"fisheye-equidistant",
|
|
39
40
|
"fisheye-equisolid-angle",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-projection.js","sourceRoot":"","sources":["../../src/camera/lens-projection.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Ideal lens projection (mapping) functions. They convert between a scene direction and a point on the\n * image plane.\n *\n * A radial projection maps the field angle theta (the angle from the optical axis) to the image\n * radius r:\n * rectilinear r = f tan(theta) (gnomonic / pinhole)\n * equidistant r = f theta (\"f-theta\" fisheye)\n * equisolid-angle r = 2 f sin(theta / 2) (equal-area fisheye)\n * stereographic r = 2 f tan(theta / 2) (conformal fisheye)\n * orthographic r = f sin(theta)\n * The image point keeps the direction's azimuth about the optical axis, so every radial projection is\n * rotationally symmetric. Equirectangular (spherical, or cylindrical equidistant) is not radial:\n * x = f * longitude, y = f * latitude, where f is the image distance per radian.\n *\n * Sources: Kannala & Brandt 2006 (IEEE TPAMI 28:1335, generic camera model listing these\n * projections, eqs. 1-5; free author copy); Miyamoto 1964 (JOSA 54:1060, fish-eye projection\n * functions); Snyder 1987 (Map Projections - A Working Manual, USGS PP 1395, free) for the\n * equirectangular and stereographic projections. Also see: Kingslake 1989 (A History of the\n * Photographic Lens, ch. on fish-eye lenses); Ray 2002 (Applied Photographic Optics, 3rd ed., ch. 6\n * and 47).\n *\n * These are ideal mapping functions. Real fisheye lenses deviate from them. Modelling a real lens\n * would need a calibrated polynomial (for example Kannala-Brandt) with defensible provenance.\n *\n * Camera frame: +X right, +Y up, +Z along the optical axis toward the scene. Image-plane coordinates\n * use the same non-inverted +X right / +Y up convention as the rest of the engine.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { rejectUnknownFields, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Ideal projection model discriminator. `rectilinear` is the engine default. */\nexport type LensProjectionModel =\n | \"rectilinear\"\n | \"fisheye-equidistant\"\n | \"fisheye-equisolid-angle\"\n | \"fisheye-stereographic\"\n | \"fisheye-orthographic\"\n | \"equirectangular\";\n\n/** Radial (rotationally symmetric) projection models. */\nexport type RadialLensProjectionModel = Exclude<\n LensProjectionModel,\n \"equirectangular\"\n>;\n\nexport const LENS_PROJECTION_MODELS: readonly LensProjectionModel[] =\n Object.freeze([\n \"rectilinear\",\n \"fisheye-equidistant\",\n \"fisheye-equisolid-angle\",\n \"fisheye-stereographic\",\n \"fisheye-orthographic\",\n \"equirectangular\"\n ]);\n\nexport interface LensDirection {\n /** Camera-frame direction component, +X right. Any positive length. */\n x: number;\n /** Camera-frame direction component, +Y up. */\n y: number;\n /** Camera-frame direction component along the optical axis toward the scene. */\n z: number;\n}\n\nexport interface CalculateLensProjectionImagePointInput {\n projectionModel: LensProjectionModel;\n /**\n * Projection scale in millimetres: the focal length f of the mapping function, or for\n * equirectangular the image distance per radian.\n */\n focalLengthMm: number;\n /** Scene direction in the camera frame. Need not be normalized; must be non-zero. */\n direction: LensDirection;\n}\n\nexport interface LensProjectionImagePoint {\n projectionModel: LensProjectionModel;\n imagePointMm: { x: number; y: number };\n /** Angle between the direction and the optical axis, in radians. */\n fieldAngleRadians: number;\n /** Distance of the image point from the optical axis, in millimetres. */\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionDirectionInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Image-plane point relative to the optical axis, in millimetres. */\n imagePointMm: { x: number; y: number };\n}\n\nexport interface LensProjectionDirection {\n projectionModel: LensProjectionModel;\n /** Unit direction in the camera frame. */\n direction: LensDirection;\n fieldAngleRadians: number;\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Centred sensor/capture dimension along one axis through the optical axis, in mm. */\n sensorDimensionMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewBoundsInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Signed minimum sensor coordinate along one axis through the optical axis, in mm. */\n minimumSensorCoordinateMm: number;\n /** Signed maximum sensor coordinate along the same axis, in mm. */\n maximumSensorCoordinateMm: number;\n}\n\nexport interface LensProjectionFieldOfView {\n projectionModel: LensProjectionModel;\n degrees: number;\n radians: number;\n /** Projection scale used by the mapping, in millimetres. */\n projectionDistanceMm: number;\n}\n\nexport interface LensProjectionFieldOfViewBounds {\n projectionModel: LensProjectionModel;\n minimumRadians: number;\n maximumRadians: number;\n radians: number;\n minimumDegrees: number;\n maximumDegrees: number;\n degrees: number;\n projectionDistanceMm: number;\n}\n\nconst HALF_PI = Math.PI / 2;\n\n/** Runtime parser for the projection discriminator. Rejects (fails closed on) unknown values. */\nexport function parseLensProjectionModel(\n value: unknown\n): LensProjectionModel {\n if (\n typeof value !== \"string\" ||\n !(LENS_PROJECTION_MODELS as readonly string[]).includes(value)\n ) {\n throw new InvalidConfigurationError(\n \"projectionModel must be one of \" +\n LENS_PROJECTION_MODELS.join(\", \") +\n \".\"\n );\n }\n return value as LensProjectionModel;\n}\n\nfunction requireRecord(name: string, value: unknown): void {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(`${name} must be an object.`);\n }\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be a finite number.`);\n }\n return value;\n}\n\n/**\n * Largest field angle (radians) a radial model maps, and whether the model can reach it.\n * Rectilinear, stereographic, equidistant and equisolid exclude the limit: the radius there is\n * infinite, or the azimuth of the backward direction is undefined. Orthographic reaches 90 degrees.\n */\nfunction radialFieldAngleLimit(model: RadialLensProjectionModel): {\n radians: number;\n inclusive: boolean;\n} {\n switch (model) {\n case \"rectilinear\":\n return { radians: HALF_PI, inclusive: false };\n case \"fisheye-orthographic\":\n return { radians: HALF_PI, inclusive: true };\n default:\n return { radians: Math.PI, inclusive: false };\n }\n}\n\n/** Image radius r(theta) for one radial model. */\nexport function radialProjectionImageRadius(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n fieldAngleRadians: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return focalLengthMm * Math.tan(fieldAngleRadians);\n case \"fisheye-equidistant\":\n return focalLengthMm * fieldAngleRadians;\n case \"fisheye-equisolid-angle\":\n return 2 * focalLengthMm * Math.sin(fieldAngleRadians / 2);\n case \"fisheye-stereographic\":\n return 2 * focalLengthMm * Math.tan(fieldAngleRadians / 2);\n case \"fisheye-orthographic\":\n return focalLengthMm * Math.sin(fieldAngleRadians);\n }\n}\n\n/**\n * Largest image radius a radial model reaches inside its field-angle domain, in mm\n * (Infinity for rectilinear and stereographic).\n */\nfunction radialImageRadiusLimit(\n model: RadialLensProjectionModel,\n focalLengthMm: number\n): { radiusMm: number; inclusive: boolean } {\n switch (model) {\n case \"rectilinear\":\n case \"fisheye-stereographic\":\n return { radiusMm: Number.POSITIVE_INFINITY, inclusive: false };\n case \"fisheye-equidistant\":\n return { radiusMm: Math.PI * focalLengthMm, inclusive: false };\n case \"fisheye-equisolid-angle\":\n return { radiusMm: 2 * focalLengthMm, inclusive: false };\n case \"fisheye-orthographic\":\n return { radiusMm: focalLengthMm, inclusive: true };\n }\n}\n\n/** Inverse theta(r) for one radial model; throws outside the image circle. */\nexport function radialProjectionFieldAngle(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n imageRadiusMm: number\n): number {\n const limit = radialImageRadiusLimit(model, focalLengthMm);\n if (\n imageRadiusMm < 0 ||\n imageRadiusMm > limit.radiusMm ||\n (!limit.inclusive && imageRadiusMm === limit.radiusMm)\n ) {\n throw new InvalidScientificInputError(\n `Image radius lies outside the ${model} image circle.`\n );\n }\n switch (model) {\n case \"rectilinear\":\n return Math.atan(imageRadiusMm / focalLengthMm);\n case \"fisheye-equidistant\":\n return imageRadiusMm / focalLengthMm;\n case \"fisheye-equisolid-angle\":\n return 2 * Math.asin(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-stereographic\":\n return 2 * Math.atan(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-orthographic\":\n return Math.asin(Math.min(1, imageRadiusMm / focalLengthMm));\n }\n}\n\nfunction assumptions(model: LensProjectionModel): readonly string[] {\n const common = [\n \"Ideal mapping function of a perfect projection; real lenses deviate from it and need calibrated distortion data\",\n \"Camera frame +X right, +Y up, +Z toward the scene; image plane +X right, +Y up (non-inverted)\"\n ];\n if (model === \"equirectangular\") {\n return [\n \"Equirectangular (spherical) projection: x = f * longitude, y = f * latitude, longitude measured about +Y from +Z\",\n ...common\n ];\n }\n return [\n `Radial projection ${model}; image azimuth equals scene azimuth about the optical axis`,\n ...common\n ];\n}\n\nfunction validateCommon(\n input: { projectionModel: unknown; focalLengthMm: number },\n name: string\n): LensProjectionModel {\n requireRecord(name, input);\n const model = parseLensProjectionModel(input.projectionModel);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n return model;\n}\n\n/**\n * Maps one camera-frame scene direction to its ideal image-plane point under the declared\n * projection model.\n */\nexport function calculateLensProjectionImagePoint(\n input: CalculateLensProjectionImagePointInput\n): CalculationResult<LensProjectionImagePoint> {\n requireInputKeys(input, \"calculateLensProjectionImagePoint\", [\"projectionModel\", \"focalLengthMm\", \"direction\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"direction\", input.direction);\n rejectUnknownFields(input.direction, [\"x\", \"y\", \"z\"], \"direction\");\n const x = requireFinite(\"direction.x\", input.direction.x);\n const y = requireFinite(\"direction.y\", input.direction.y);\n const z = requireFinite(\"direction.z\", input.direction.z);\n const length = Math.hypot(x, y, z);\n if (!(length > 0) || !Number.isFinite(length)) {\n throw new InvalidScientificInputError(\n \"direction must be a finite non-zero vector.\"\n );\n }\n const { imagePointMm, fieldAngleRadians } = lensProjectionImagePointValue(\n model,\n input.focalLengthMm,\n x,\n y,\n z\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n imagePointMm,\n fieldAngleRadians,\n imageRadiusMm: Math.hypot(imagePointMm.x, imagePointMm.y)\n },\n `${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/**\n * Inverse-maps one image-plane point to its unit camera-frame scene direction under the declared\n * projection model. It rejects points outside the model's image circle. For equirectangular, it\n * rejects points outside +/-pi longitude and +/-pi/2 latitude.\n */\nexport function calculateLensProjectionDirection(\n input: CalculateLensProjectionDirectionInput\n): CalculationResult<LensProjectionDirection> {\n requireInputKeys(input, \"calculateLensProjectionDirection\", [\"projectionModel\", \"focalLengthMm\", \"imagePointMm\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"imagePointMm\", input.imagePointMm);\n rejectUnknownFields(input.imagePointMm, [\"x\", \"y\"], \"imagePointMm\");\n const px = requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n const py = requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n const imageRadiusMm = Math.hypot(px, py);\n const direction = lensProjectionDirectionValue(\n model,\n input.focalLengthMm,\n px,\n py\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n direction,\n fieldAngleRadians: Math.atan2(\n Math.hypot(direction.x, direction.y),\n direction.z\n ),\n imageRadiusMm\n },\n `inverse-${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/** Signed angle subtended along one axis through the optical axis for a signed coordinate. */\nfunction signedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n if (model === \"equirectangular\") {\n // Along a horizontal or vertical line through the centre the angle is linear in position.\n return coordinateMm / focalLengthMm;\n }\n const theta = radialProjectionFieldAngle(\n model,\n focalLengthMm,\n Math.abs(coordinateMm)\n );\n return coordinateMm < 0 ? -theta : theta;\n}\n\n/**\n * Angular field of view across a centred sensor dimension along one axis through the optical\n * axis: 2 * theta(d / 2) for radial models and d / f for equirectangular.\n *\n * For equirectangular, the result is the longitude span for a horizontal dimension and the latitude\n * span for a vertical one. A sensor dimension larger than a fisheye's image circle is rejected, not\n * clipped.\n */\nexport function calculateLensProjectionFieldOfView(\n input: CalculateLensProjectionFieldOfViewInput\n): CalculationResult<LensProjectionFieldOfView> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfView\", [\"projectionModel\", \"focalLengthMm\", \"sensorDimensionMm\"]);\n const model = validateCommon(input, \"input\");\n requirePositiveFinite(\"sensorDimensionMm\", input.sensorDimensionMm);\n const radians =\n 2 *\n signedAxisAngle(model, input.focalLengthMm, input.sensorDimensionMm / 2);\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"sensorDimensionMm exceeds the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n radians,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Field of view is measured along one axis through the optical axis\"\n ]\n );\n}\n\n/**\n * Asymmetric angular span between two signed sensor coordinates on one axis through the optical\n * axis, for off-centre crops.\n */\nexport function calculateLensProjectionFieldOfViewBounds(\n input: CalculateLensProjectionFieldOfViewBoundsInput\n): CalculationResult<LensProjectionFieldOfViewBounds> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfViewBounds\", [\"projectionModel\", \"focalLengthMm\", \"minimumSensorCoordinateMm\", \"maximumSensorCoordinateMm\"]);\n const model = validateCommon(input, \"input\");\n const minimum = requireFinite(\n \"minimumSensorCoordinateMm\",\n input.minimumSensorCoordinateMm\n );\n const maximum = requireFinite(\n \"maximumSensorCoordinateMm\",\n input.maximumSensorCoordinateMm\n );\n if (maximum <= minimum) {\n throw new InvalidScientificInputError(\n \"maximumSensorCoordinateMm must be greater than minimumSensorCoordinateMm.\"\n );\n }\n const minimumRadians = signedAxisAngle(model, input.focalLengthMm, minimum);\n const maximumRadians = signedAxisAngle(model, input.focalLengthMm, maximum);\n const radians = maximumRadians - minimumRadians;\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"Sensor bounds exceed the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n minimumRadians,\n maximumRadians,\n radians,\n minimumDegrees: (minimumRadians * 180) / Math.PI,\n maximumDegrees: (maximumRadians * 180) / Math.PI,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `asymmetric-${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Signed sensor-plane bounds are measured relative to the optical axis along one axis through it\"\n ]\n );\n}\n\n/**\n * @internal Image point of a non-zero camera-frame direction under one model (no envelope).\n * Shared by the public mapping, geometric remaps and the environment ray query.\n */\nexport function lensProjectionImagePointValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n x: number,\n y: number,\n z: number\n): { imagePointMm: { x: number; y: number }; fieldAngleRadians: number } {\n const length = Math.hypot(x, y, z);\n const f = focalLengthMm;\n const transverse = Math.hypot(x, y);\n const fieldAngleRadians = Math.atan2(transverse, z);\n\n let imagePointMm: { x: number; y: number };\n if (model === \"equirectangular\") {\n const latitude = Math.asin(Math.max(-1, Math.min(1, y / length)));\n const horizontal = Math.hypot(x, z);\n if (horizontal === 0) {\n // Poles have an undefined longitude; the projection is singular there.\n throw new InvalidScientificInputError(\n \"Equirectangular projection is undefined at the poles (direction parallel to +/-Y).\"\n );\n }\n const longitude = Math.atan2(x, z);\n imagePointMm = { x: f * longitude, y: f * latitude };\n } else {\n const limit = radialFieldAngleLimit(model);\n if (\n fieldAngleRadians > limit.radians ||\n (!limit.inclusive && fieldAngleRadians === limit.radians)\n ) {\n throw new InvalidScientificInputError(\n `direction lies outside the ${model} projection domain.`\n );\n }\n const radius = radialProjectionImageRadius(model, f, fieldAngleRadians);\n imagePointMm =\n transverse === 0\n ? { x: 0, y: 0 }\n : { x: (radius * x) / transverse, y: (radius * y) / transverse };\n }\n\n return { imagePointMm, fieldAngleRadians };\n}\n\n/** @internal Unit camera-frame direction of one image point under one model (no envelope). */\nexport function lensProjectionDirectionValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n px: number,\n py: number\n): LensDirection {\n const f = focalLengthMm;\n const imageRadiusMm = Math.hypot(px, py);\n\n let direction: LensDirection;\n if (model === \"equirectangular\") {\n const longitude = px / f;\n const latitude = py / f;\n if (Math.abs(longitude) > Math.PI || Math.abs(latitude) > HALF_PI) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the equirectangular longitude/latitude domain.\"\n );\n }\n const cosLatitude = Math.cos(latitude);\n direction = {\n x: cosLatitude * Math.sin(longitude),\n y: Math.sin(latitude),\n z: cosLatitude * Math.cos(longitude)\n };\n } else {\n const theta = radialProjectionFieldAngle(model, f, imageRadiusMm);\n const sinTheta = Math.sin(theta);\n direction =\n imageRadiusMm === 0\n ? { x: 0, y: 0, z: 1 }\n : {\n x: (sinTheta * px) / imageRadiusMm,\n y: (sinTheta * py) / imageRadiusMm,\n z: Math.cos(theta)\n };\n }\n\n return direction;\n}\n\n/** d r / d theta of a radial model. */\nfunction radialDerivative(\n model: RadialLensProjectionModel,\n f: number,\n theta: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return f / Math.cos(theta) ** 2;\n case \"fisheye-equidistant\":\n return f;\n case \"fisheye-equisolid-angle\":\n return f * Math.cos(theta / 2);\n case \"fisheye-stereographic\":\n return f / Math.cos(theta / 2) ** 2;\n case \"fisheye-orthographic\":\n return f * Math.cos(theta);\n }\n}\n\ntype Matrix32 = readonly [readonly [number, number], readonly [number, number], readonly [number, number]];\n\n/** Unit direction of an image point and its derivative d(direction)/d(point) (3 x 2). */\nfunction directionWithDerivative(\n model: LensProjectionModel,\n f: number,\n px: number,\n py: number\n): { direction: LensDirection; derivative: Matrix32 } {\n const direction = lensProjectionDirectionValue(model, f, px, py);\n if (model === \"equirectangular\") {\n const lambda = px / f;\n const phi = py / f;\n const cp = Math.cos(phi);\n const sp = Math.sin(phi);\n const cl = Math.cos(lambda);\n const sl = Math.sin(lambda);\n return {\n direction,\n derivative: [\n [(cp * cl) / f, (-sp * sl) / f],\n [0, cp / f],\n [(-cp * sl) / f, (-sp * cl) / f]\n ]\n };\n }\n const rho = Math.hypot(px, py);\n if (rho === 0) {\n const a = 1 / radialDerivative(model, f, 0);\n return { direction, derivative: [[a, 0], [0, a], [0, 0]] };\n }\n const theta = radialProjectionFieldAngle(model, f, rho);\n const dTheta = 1 / radialDerivative(model, f, theta);\n const a = Math.sin(theta) / rho;\n const aPrime = (Math.cos(theta) * dTheta * rho - Math.sin(theta)) / (rho * rho);\n const ux = px / rho;\n const uy = py / rho;\n const dz = -Math.sin(theta) * dTheta;\n return {\n direction,\n derivative: [\n [a + aPrime * px * ux, aPrime * px * uy],\n [aPrime * py * ux, a + aPrime * py * uy],\n [dz * ux, dz * uy]\n ]\n };\n}\n\n/** Image point of a unit direction and its derivative d(point)/d(direction) (2 x 3). */\nfunction imagePointWithDerivative(\n model: LensProjectionModel,\n f: number,\n v: LensDirection\n): { point: { x: number; y: number }; derivative: readonly [readonly [number, number, number], readonly [number, number, number]] } {\n const point = lensProjectionImagePointValue(model, f, v.x, v.y, v.z).imagePointMm;\n if (model === \"equirectangular\") {\n const h2 = v.x * v.x + v.z * v.z;\n const c = Math.sqrt(Math.max(0, 1 - v.y * v.y));\n return {\n point,\n derivative: [\n [(f * v.z) / h2, 0, (-f * v.x) / h2],\n [0, f / c, 0]\n ]\n };\n }\n const t = Math.hypot(v.x, v.y);\n const theta = Math.atan2(t, v.z);\n if (theta < 1e-9) {\n const g = radialDerivative(model, f, 0);\n return { point, derivative: [[g, 0, 0], [0, g, 0]] };\n }\n const s = Math.sin(theta);\n const g = radialProjectionImageRadius(model, f, theta);\n const b = g / s;\n const bPrime = (radialDerivative(model, f, theta) * s - g * Math.cos(theta)) / (s * s);\n // q = b(theta) (vx, vy); on the unit sphere d theta = -d vz / sin theta.\n const k = -bPrime / s;\n return {\n point,\n derivative: [\n [b, 0, k * v.x],\n [0, b, k * v.y]\n ]\n };\n}\n\n/**\n * @internal Destination-to-source remap between two ideal projections: the destination point is\n * unprojected with the destination model and reprojected with the source model. Returns the\n * source point and the 2 x 2 Jacobian d(source)/d(destination), row-major.\n */\nexport function lensProjectionRemapValue(\n destinationModel: LensProjectionModel,\n destinationFocalLengthMm: number,\n sourceModel: LensProjectionModel,\n sourceFocalLengthMm: number,\n px: number,\n py: number\n): { sourcePointMm: { x: number; y: number }; jacobian: readonly [number, number, number, number] } {\n const inverse = directionWithDerivative(destinationModel, destinationFocalLengthMm, px, py);\n const forward = imagePointWithDerivative(sourceModel, sourceFocalLengthMm, inverse.direction);\n const d = inverse.derivative;\n const q = forward.derivative;\n const j = (row: 0 | 1, column: 0 | 1): number =>\n q[row][0] * d[0][column] + q[row][1] * d[1][column] + q[row][2] * d[2][column];\n return {\n sourcePointMm: forward.point,\n jacobian: [j(0, 0), j(0, 1), j(1, 0), j(1, 1)]\n };\n}\n\n/** @internal Signed angle along one axis through the optical axis (exported for sampling plans). */\nexport function lensProjectionSignedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n return signedAxisAngle(model, focalLengthMm, coordinateMm);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lens-projection.js","sourceRoot":"","sources":["../../src/camera/lens-projection.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Ideal lens projection (mapping) functions. They convert between a scene direction and a point on the\n * image plane.\n *\n * A radial projection maps the field angle theta (the angle from the optical axis) to the image\n * radius r:\n * rectilinear r = f tan(theta) (gnomonic / pinhole)\n * equidistant r = f theta (\"f-theta\" fisheye)\n * equisolid-angle r = 2 f sin(theta / 2) (equal-area fisheye)\n * stereographic r = 2 f tan(theta / 2) (conformal fisheye)\n * orthographic r = f sin(theta)\n * The image point keeps the direction's azimuth about the optical axis, so every radial projection is\n * rotationally symmetric. Equirectangular (spherical, or cylindrical equidistant) is not radial:\n * x = f * longitude, y = f * latitude, where f is the image distance per radian.\n *\n * Sources: Kannala & Brandt 2006 (IEEE TPAMI 28:1335, generic camera model listing these\n * projections, eqs. 1-5; free author copy); Miyamoto 1964 (JOSA 54:1060, fish-eye projection\n * functions); Snyder 1987 (Map Projections - A Working Manual, USGS PP 1395, free) for the\n * equirectangular and stereographic projections. Also see: Kingslake 1989 (A History of the\n * Photographic Lens, ch. on fish-eye lenses); Ray 2002 (Applied Photographic Optics, 3rd ed., ch. 6\n * and 47).\n *\n * These are ideal mapping functions. Real fisheye lenses deviate from them. Modelling a real lens\n * would need a calibrated polynomial (for example Kannala-Brandt) with defensible provenance.\n *\n * Camera frame: +X right, +Y up, +Z along the optical axis toward the scene. Image-plane coordinates\n * use the same non-inverted +X right / +Y up convention as the rest of the engine.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { rejectUnknownFields, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Ideal projection model discriminator. `rectilinear` is the engine default. */\nexport type LensProjectionModel =\n | \"rectilinear\"\n | \"fisheye-equidistant\"\n | \"fisheye-equisolid-angle\"\n | \"fisheye-stereographic\"\n | \"fisheye-orthographic\"\n | \"equirectangular\";\n\n/** Radial (rotationally symmetric) projection models. */\nexport type RadialLensProjectionModel = Exclude<\n LensProjectionModel,\n \"equirectangular\"\n>;\n\nexport const LENS_PROJECTION_MODELS: readonly LensProjectionModel[] =\n /* @__PURE__ */ Object.freeze([\n \"rectilinear\",\n \"fisheye-equidistant\",\n \"fisheye-equisolid-angle\",\n \"fisheye-stereographic\",\n \"fisheye-orthographic\",\n \"equirectangular\"\n ]);\n\nexport interface LensDirection {\n /** Camera-frame direction component, +X right. Any positive length. */\n x: number;\n /** Camera-frame direction component, +Y up. */\n y: number;\n /** Camera-frame direction component along the optical axis toward the scene. */\n z: number;\n}\n\nexport interface CalculateLensProjectionImagePointInput {\n projectionModel: LensProjectionModel;\n /**\n * Projection scale in millimetres: the focal length f of the mapping function, or for\n * equirectangular the image distance per radian.\n */\n focalLengthMm: number;\n /** Scene direction in the camera frame. Need not be normalized; must be non-zero. */\n direction: LensDirection;\n}\n\nexport interface LensProjectionImagePoint {\n projectionModel: LensProjectionModel;\n imagePointMm: { x: number; y: number };\n /** Angle between the direction and the optical axis, in radians. */\n fieldAngleRadians: number;\n /** Distance of the image point from the optical axis, in millimetres. */\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionDirectionInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Image-plane point relative to the optical axis, in millimetres. */\n imagePointMm: { x: number; y: number };\n}\n\nexport interface LensProjectionDirection {\n projectionModel: LensProjectionModel;\n /** Unit direction in the camera frame. */\n direction: LensDirection;\n fieldAngleRadians: number;\n imageRadiusMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Centred sensor/capture dimension along one axis through the optical axis, in mm. */\n sensorDimensionMm: number;\n}\n\nexport interface CalculateLensProjectionFieldOfViewBoundsInput {\n projectionModel: LensProjectionModel;\n focalLengthMm: number;\n /** Signed minimum sensor coordinate along one axis through the optical axis, in mm. */\n minimumSensorCoordinateMm: number;\n /** Signed maximum sensor coordinate along the same axis, in mm. */\n maximumSensorCoordinateMm: number;\n}\n\nexport interface LensProjectionFieldOfView {\n projectionModel: LensProjectionModel;\n degrees: number;\n radians: number;\n /** Projection scale used by the mapping, in millimetres. */\n projectionDistanceMm: number;\n}\n\nexport interface LensProjectionFieldOfViewBounds {\n projectionModel: LensProjectionModel;\n minimumRadians: number;\n maximumRadians: number;\n radians: number;\n minimumDegrees: number;\n maximumDegrees: number;\n degrees: number;\n projectionDistanceMm: number;\n}\n\nconst HALF_PI = Math.PI / 2;\n\n/** Runtime parser for the projection discriminator. Rejects (fails closed on) unknown values. */\nexport function parseLensProjectionModel(\n value: unknown\n): LensProjectionModel {\n if (\n typeof value !== \"string\" ||\n !(LENS_PROJECTION_MODELS as readonly string[]).includes(value)\n ) {\n throw new InvalidConfigurationError(\n \"projectionModel must be one of \" +\n LENS_PROJECTION_MODELS.join(\", \") +\n \".\"\n );\n }\n return value as LensProjectionModel;\n}\n\nfunction requireRecord(name: string, value: unknown): void {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(`${name} must be an object.`);\n }\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be a finite number.`);\n }\n return value;\n}\n\n/**\n * Largest field angle (radians) a radial model maps, and whether the model can reach it.\n * Rectilinear, stereographic, equidistant and equisolid exclude the limit: the radius there is\n * infinite, or the azimuth of the backward direction is undefined. Orthographic reaches 90 degrees.\n */\nfunction radialFieldAngleLimit(model: RadialLensProjectionModel): {\n radians: number;\n inclusive: boolean;\n} {\n switch (model) {\n case \"rectilinear\":\n return { radians: HALF_PI, inclusive: false };\n case \"fisheye-orthographic\":\n return { radians: HALF_PI, inclusive: true };\n default:\n return { radians: Math.PI, inclusive: false };\n }\n}\n\n/** Image radius r(theta) for one radial model. */\nexport function radialProjectionImageRadius(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n fieldAngleRadians: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return focalLengthMm * Math.tan(fieldAngleRadians);\n case \"fisheye-equidistant\":\n return focalLengthMm * fieldAngleRadians;\n case \"fisheye-equisolid-angle\":\n return 2 * focalLengthMm * Math.sin(fieldAngleRadians / 2);\n case \"fisheye-stereographic\":\n return 2 * focalLengthMm * Math.tan(fieldAngleRadians / 2);\n case \"fisheye-orthographic\":\n return focalLengthMm * Math.sin(fieldAngleRadians);\n }\n}\n\n/**\n * Largest image radius a radial model reaches inside its field-angle domain, in mm\n * (Infinity for rectilinear and stereographic).\n */\nfunction radialImageRadiusLimit(\n model: RadialLensProjectionModel,\n focalLengthMm: number\n): { radiusMm: number; inclusive: boolean } {\n switch (model) {\n case \"rectilinear\":\n case \"fisheye-stereographic\":\n return { radiusMm: Number.POSITIVE_INFINITY, inclusive: false };\n case \"fisheye-equidistant\":\n return { radiusMm: Math.PI * focalLengthMm, inclusive: false };\n case \"fisheye-equisolid-angle\":\n return { radiusMm: 2 * focalLengthMm, inclusive: false };\n case \"fisheye-orthographic\":\n return { radiusMm: focalLengthMm, inclusive: true };\n }\n}\n\n/** Inverse theta(r) for one radial model; throws outside the image circle. */\nexport function radialProjectionFieldAngle(\n model: RadialLensProjectionModel,\n focalLengthMm: number,\n imageRadiusMm: number\n): number {\n const limit = radialImageRadiusLimit(model, focalLengthMm);\n if (\n imageRadiusMm < 0 ||\n imageRadiusMm > limit.radiusMm ||\n (!limit.inclusive && imageRadiusMm === limit.radiusMm)\n ) {\n throw new InvalidScientificInputError(\n `Image radius lies outside the ${model} image circle.`\n );\n }\n switch (model) {\n case \"rectilinear\":\n return Math.atan(imageRadiusMm / focalLengthMm);\n case \"fisheye-equidistant\":\n return imageRadiusMm / focalLengthMm;\n case \"fisheye-equisolid-angle\":\n return 2 * Math.asin(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-stereographic\":\n return 2 * Math.atan(imageRadiusMm / (2 * focalLengthMm));\n case \"fisheye-orthographic\":\n return Math.asin(Math.min(1, imageRadiusMm / focalLengthMm));\n }\n}\n\nfunction assumptions(model: LensProjectionModel): readonly string[] {\n const common = [\n \"Ideal mapping function of a perfect projection; real lenses deviate from it and need calibrated distortion data\",\n \"Camera frame +X right, +Y up, +Z toward the scene; image plane +X right, +Y up (non-inverted)\"\n ];\n if (model === \"equirectangular\") {\n return [\n \"Equirectangular (spherical) projection: x = f * longitude, y = f * latitude, longitude measured about +Y from +Z\",\n ...common\n ];\n }\n return [\n `Radial projection ${model}; image azimuth equals scene azimuth about the optical axis`,\n ...common\n ];\n}\n\nfunction validateCommon(\n input: { projectionModel: unknown; focalLengthMm: number },\n name: string\n): LensProjectionModel {\n requireRecord(name, input);\n const model = parseLensProjectionModel(input.projectionModel);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n return model;\n}\n\n/**\n * Maps one camera-frame scene direction to its ideal image-plane point under the declared\n * projection model.\n */\nexport function calculateLensProjectionImagePoint(\n input: CalculateLensProjectionImagePointInput\n): CalculationResult<LensProjectionImagePoint> {\n requireInputKeys(input, \"calculateLensProjectionImagePoint\", [\"projectionModel\", \"focalLengthMm\", \"direction\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"direction\", input.direction);\n rejectUnknownFields(input.direction, [\"x\", \"y\", \"z\"], \"direction\");\n const x = requireFinite(\"direction.x\", input.direction.x);\n const y = requireFinite(\"direction.y\", input.direction.y);\n const z = requireFinite(\"direction.z\", input.direction.z);\n const length = Math.hypot(x, y, z);\n if (!(length > 0) || !Number.isFinite(length)) {\n throw new InvalidScientificInputError(\n \"direction must be a finite non-zero vector.\"\n );\n }\n const { imagePointMm, fieldAngleRadians } = lensProjectionImagePointValue(\n model,\n input.focalLengthMm,\n x,\n y,\n z\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n imagePointMm,\n fieldAngleRadians,\n imageRadiusMm: Math.hypot(imagePointMm.x, imagePointMm.y)\n },\n `${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/**\n * Inverse-maps one image-plane point to its unit camera-frame scene direction under the declared\n * projection model. It rejects points outside the model's image circle. For equirectangular, it\n * rejects points outside +/-pi longitude and +/-pi/2 latitude.\n */\nexport function calculateLensProjectionDirection(\n input: CalculateLensProjectionDirectionInput\n): CalculationResult<LensProjectionDirection> {\n requireInputKeys(input, \"calculateLensProjectionDirection\", [\"projectionModel\", \"focalLengthMm\", \"imagePointMm\"]);\n const model = validateCommon(input, \"input\");\n requireRecord(\"imagePointMm\", input.imagePointMm);\n rejectUnknownFields(input.imagePointMm, [\"x\", \"y\"], \"imagePointMm\");\n const px = requireFinite(\"imagePointMm.x\", input.imagePointMm.x);\n const py = requireFinite(\"imagePointMm.y\", input.imagePointMm.y);\n const imageRadiusMm = Math.hypot(px, py);\n const direction = lensProjectionDirectionValue(\n model,\n input.focalLengthMm,\n px,\n py\n );\n\n return calculatedResult(\n {\n projectionModel: model,\n direction,\n fieldAngleRadians: Math.atan2(\n Math.hypot(direction.x, direction.y),\n direction.z\n ),\n imageRadiusMm\n },\n `inverse-${model}-lens-projection`,\n \"1.0.0\",\n assumptions(model)\n );\n}\n\n/** Signed angle subtended along one axis through the optical axis for a signed coordinate. */\nfunction signedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n if (model === \"equirectangular\") {\n // Along a horizontal or vertical line through the centre the angle is linear in position.\n return coordinateMm / focalLengthMm;\n }\n const theta = radialProjectionFieldAngle(\n model,\n focalLengthMm,\n Math.abs(coordinateMm)\n );\n return coordinateMm < 0 ? -theta : theta;\n}\n\n/**\n * Angular field of view across a centred sensor dimension along one axis through the optical\n * axis: 2 * theta(d / 2) for radial models and d / f for equirectangular.\n *\n * For equirectangular, the result is the longitude span for a horizontal dimension and the latitude\n * span for a vertical one. A sensor dimension larger than a fisheye's image circle is rejected, not\n * clipped.\n */\nexport function calculateLensProjectionFieldOfView(\n input: CalculateLensProjectionFieldOfViewInput\n): CalculationResult<LensProjectionFieldOfView> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfView\", [\"projectionModel\", \"focalLengthMm\", \"sensorDimensionMm\"]);\n const model = validateCommon(input, \"input\");\n requirePositiveFinite(\"sensorDimensionMm\", input.sensorDimensionMm);\n const radians =\n 2 *\n signedAxisAngle(model, input.focalLengthMm, input.sensorDimensionMm / 2);\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"sensorDimensionMm exceeds the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n radians,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Field of view is measured along one axis through the optical axis\"\n ]\n );\n}\n\n/**\n * Asymmetric angular span between two signed sensor coordinates on one axis through the optical\n * axis, for off-centre crops.\n */\nexport function calculateLensProjectionFieldOfViewBounds(\n input: CalculateLensProjectionFieldOfViewBoundsInput\n): CalculationResult<LensProjectionFieldOfViewBounds> {\n requireInputKeys(input, \"calculateLensProjectionFieldOfViewBounds\", [\"projectionModel\", \"focalLengthMm\", \"minimumSensorCoordinateMm\", \"maximumSensorCoordinateMm\"]);\n const model = validateCommon(input, \"input\");\n const minimum = requireFinite(\n \"minimumSensorCoordinateMm\",\n input.minimumSensorCoordinateMm\n );\n const maximum = requireFinite(\n \"maximumSensorCoordinateMm\",\n input.maximumSensorCoordinateMm\n );\n if (maximum <= minimum) {\n throw new InvalidScientificInputError(\n \"maximumSensorCoordinateMm must be greater than minimumSensorCoordinateMm.\"\n );\n }\n const minimumRadians = signedAxisAngle(model, input.focalLengthMm, minimum);\n const maximumRadians = signedAxisAngle(model, input.focalLengthMm, maximum);\n const radians = maximumRadians - minimumRadians;\n if (model === \"equirectangular\" && radians > 2 * Math.PI) {\n throw new InvalidScientificInputError(\n \"Sensor bounds exceed the full 360-degree equirectangular span.\"\n );\n }\n return calculatedResult(\n {\n projectionModel: model,\n minimumRadians,\n maximumRadians,\n radians,\n minimumDegrees: (minimumRadians * 180) / Math.PI,\n maximumDegrees: (maximumRadians * 180) / Math.PI,\n degrees: (radians * 180) / Math.PI,\n projectionDistanceMm: input.focalLengthMm\n },\n `asymmetric-${model}-field-of-view`,\n \"1.0.0\",\n [\n ...assumptions(model),\n \"Signed sensor-plane bounds are measured relative to the optical axis along one axis through it\"\n ]\n );\n}\n\n/**\n * @internal Image point of a non-zero camera-frame direction under one model (no envelope).\n * Shared by the public mapping, geometric remaps and the environment ray query.\n */\nexport function lensProjectionImagePointValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n x: number,\n y: number,\n z: number\n): { imagePointMm: { x: number; y: number }; fieldAngleRadians: number } {\n const length = Math.hypot(x, y, z);\n const f = focalLengthMm;\n const transverse = Math.hypot(x, y);\n const fieldAngleRadians = Math.atan2(transverse, z);\n\n let imagePointMm: { x: number; y: number };\n if (model === \"equirectangular\") {\n const latitude = Math.asin(Math.max(-1, Math.min(1, y / length)));\n const horizontal = Math.hypot(x, z);\n if (horizontal === 0) {\n // Poles have an undefined longitude; the projection is singular there.\n throw new InvalidScientificInputError(\n \"Equirectangular projection is undefined at the poles (direction parallel to +/-Y).\"\n );\n }\n const longitude = Math.atan2(x, z);\n imagePointMm = { x: f * longitude, y: f * latitude };\n } else {\n const limit = radialFieldAngleLimit(model);\n if (\n fieldAngleRadians > limit.radians ||\n (!limit.inclusive && fieldAngleRadians === limit.radians)\n ) {\n throw new InvalidScientificInputError(\n `direction lies outside the ${model} projection domain.`\n );\n }\n const radius = radialProjectionImageRadius(model, f, fieldAngleRadians);\n imagePointMm =\n transverse === 0\n ? { x: 0, y: 0 }\n : { x: (radius * x) / transverse, y: (radius * y) / transverse };\n }\n\n return { imagePointMm, fieldAngleRadians };\n}\n\n/** @internal Unit camera-frame direction of one image point under one model (no envelope). */\nexport function lensProjectionDirectionValue(\n model: LensProjectionModel,\n focalLengthMm: number,\n px: number,\n py: number\n): LensDirection {\n const f = focalLengthMm;\n const imageRadiusMm = Math.hypot(px, py);\n\n let direction: LensDirection;\n if (model === \"equirectangular\") {\n const longitude = px / f;\n const latitude = py / f;\n if (Math.abs(longitude) > Math.PI || Math.abs(latitude) > HALF_PI) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the equirectangular longitude/latitude domain.\"\n );\n }\n const cosLatitude = Math.cos(latitude);\n direction = {\n x: cosLatitude * Math.sin(longitude),\n y: Math.sin(latitude),\n z: cosLatitude * Math.cos(longitude)\n };\n } else {\n const theta = radialProjectionFieldAngle(model, f, imageRadiusMm);\n const sinTheta = Math.sin(theta);\n direction =\n imageRadiusMm === 0\n ? { x: 0, y: 0, z: 1 }\n : {\n x: (sinTheta * px) / imageRadiusMm,\n y: (sinTheta * py) / imageRadiusMm,\n z: Math.cos(theta)\n };\n }\n\n return direction;\n}\n\n/** d r / d theta of a radial model. */\nfunction radialDerivative(\n model: RadialLensProjectionModel,\n f: number,\n theta: number\n): number {\n switch (model) {\n case \"rectilinear\":\n return f / Math.cos(theta) ** 2;\n case \"fisheye-equidistant\":\n return f;\n case \"fisheye-equisolid-angle\":\n return f * Math.cos(theta / 2);\n case \"fisheye-stereographic\":\n return f / Math.cos(theta / 2) ** 2;\n case \"fisheye-orthographic\":\n return f * Math.cos(theta);\n }\n}\n\ntype Matrix32 = readonly [readonly [number, number], readonly [number, number], readonly [number, number]];\n\n/** Unit direction of an image point and its derivative d(direction)/d(point) (3 x 2). */\nfunction directionWithDerivative(\n model: LensProjectionModel,\n f: number,\n px: number,\n py: number\n): { direction: LensDirection; derivative: Matrix32 } {\n const direction = lensProjectionDirectionValue(model, f, px, py);\n if (model === \"equirectangular\") {\n const lambda = px / f;\n const phi = py / f;\n const cp = Math.cos(phi);\n const sp = Math.sin(phi);\n const cl = Math.cos(lambda);\n const sl = Math.sin(lambda);\n return {\n direction,\n derivative: [\n [(cp * cl) / f, (-sp * sl) / f],\n [0, cp / f],\n [(-cp * sl) / f, (-sp * cl) / f]\n ]\n };\n }\n const rho = Math.hypot(px, py);\n if (rho === 0) {\n const a = 1 / radialDerivative(model, f, 0);\n return { direction, derivative: [[a, 0], [0, a], [0, 0]] };\n }\n const theta = radialProjectionFieldAngle(model, f, rho);\n const dTheta = 1 / radialDerivative(model, f, theta);\n const a = Math.sin(theta) / rho;\n const aPrime = (Math.cos(theta) * dTheta * rho - Math.sin(theta)) / (rho * rho);\n const ux = px / rho;\n const uy = py / rho;\n const dz = -Math.sin(theta) * dTheta;\n return {\n direction,\n derivative: [\n [a + aPrime * px * ux, aPrime * px * uy],\n [aPrime * py * ux, a + aPrime * py * uy],\n [dz * ux, dz * uy]\n ]\n };\n}\n\n/** Image point of a unit direction and its derivative d(point)/d(direction) (2 x 3). */\nfunction imagePointWithDerivative(\n model: LensProjectionModel,\n f: number,\n v: LensDirection\n): { point: { x: number; y: number }; derivative: readonly [readonly [number, number, number], readonly [number, number, number]] } {\n const point = lensProjectionImagePointValue(model, f, v.x, v.y, v.z).imagePointMm;\n if (model === \"equirectangular\") {\n const h2 = v.x * v.x + v.z * v.z;\n const c = Math.sqrt(Math.max(0, 1 - v.y * v.y));\n return {\n point,\n derivative: [\n [(f * v.z) / h2, 0, (-f * v.x) / h2],\n [0, f / c, 0]\n ]\n };\n }\n const t = Math.hypot(v.x, v.y);\n const theta = Math.atan2(t, v.z);\n if (theta < 1e-9) {\n const g = radialDerivative(model, f, 0);\n return { point, derivative: [[g, 0, 0], [0, g, 0]] };\n }\n const s = Math.sin(theta);\n const g = radialProjectionImageRadius(model, f, theta);\n const b = g / s;\n const bPrime = (radialDerivative(model, f, theta) * s - g * Math.cos(theta)) / (s * s);\n // q = b(theta) (vx, vy); on the unit sphere d theta = -d vz / sin theta.\n const k = -bPrime / s;\n return {\n point,\n derivative: [\n [b, 0, k * v.x],\n [0, b, k * v.y]\n ]\n };\n}\n\n/**\n * @internal Destination-to-source remap between two ideal projections: the destination point is\n * unprojected with the destination model and reprojected with the source model. Returns the\n * source point and the 2 x 2 Jacobian d(source)/d(destination), row-major.\n */\nexport function lensProjectionRemapValue(\n destinationModel: LensProjectionModel,\n destinationFocalLengthMm: number,\n sourceModel: LensProjectionModel,\n sourceFocalLengthMm: number,\n px: number,\n py: number\n): { sourcePointMm: { x: number; y: number }; jacobian: readonly [number, number, number, number] } {\n const inverse = directionWithDerivative(destinationModel, destinationFocalLengthMm, px, py);\n const forward = imagePointWithDerivative(sourceModel, sourceFocalLengthMm, inverse.direction);\n const d = inverse.derivative;\n const q = forward.derivative;\n const j = (row: 0 | 1, column: 0 | 1): number =>\n q[row][0] * d[0][column] + q[row][1] * d[1][column] + q[row][2] * d[2][column];\n return {\n sourcePointMm: forward.point,\n jacobian: [j(0, 0), j(0, 1), j(1, 0), j(1, 1)]\n };\n}\n\n/** @internal Signed angle along one axis through the optical axis (exported for sampling plans). */\nexport function lensProjectionSignedAxisAngle(\n model: LensProjectionModel,\n focalLengthMm: number,\n coordinateMm: number\n): number {\n return signedAxisAngle(model, focalLengthMm, coordinateMm);\n}\n"]}
|
|
@@ -22,7 +22,7 @@ import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
|
22
22
|
import { parseLosslessJpeg } from "./lossless-jpeg.js";
|
|
23
23
|
export const DNG_IMPORT_SCHEMA_VERSION = "1.0.0";
|
|
24
24
|
/** Upper bound for `maximumPixels`. 200 MP (megapixels) of 16-bit codes is 400 MB. */
|
|
25
|
-
export const DNG_IMPORT_LIMITS = Object.freeze({ defaultMaximumPixels: 64_000_000, maximumPixels: 200_000_000, defaultMaximumIfds: 64, maximumIfds: 1024 });
|
|
25
|
+
export const DNG_IMPORT_LIMITS = /* @__PURE__ */ Object.freeze({ defaultMaximumPixels: 64_000_000, maximumPixels: 200_000_000, defaultMaximumIfds: 64, maximumIfds: 1024 });
|
|
26
26
|
const TYPE_SIZES = [0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8, 4];
|
|
27
27
|
function fail(message) { throw new InvalidConfigurationError("DNG import: " + message); }
|
|
28
28
|
function limit(value, fallback, max, name) {
|