@photivra/engine 2.0.0 → 2.1.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 +22 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +30 -0
- package/dist/facade/diffraction-provider.js +127 -0
- package/dist/facade/diffraction-provider.js.map +1 -0
- package/dist/facade/host-yield.d.ts +1 -1
- package/dist/facade/host-yield.js +0 -4
- package/dist/facade/host-yield.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +81 -2
- package/dist/facade/simulate-capture.js +281 -26
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +118 -0
- package/dist/optics/diffraction-psf-profile.js +252 -0
- package/dist/optics/diffraction-psf-profile.js.map +1 -0
- package/docs/API_REFERENCE.md +17 -1
- package/docs/QUICK_START.md +178 -10
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +47 -2
- package/docs/api/facade-simulate-capture.md +76 -3
- package/docs/api/optics-diffraction-psf-profile.md +96 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 2.
|
|
3
|
+
## 2.1.0 — unreleased (prepared October 17, 2026)
|
|
4
|
+
|
|
5
|
+
A minor release: additive, opt-in capture options for diffraction, AHD demosaicing and an optical anti-aliasing filter, per-run cost diagnostics and a measured cost table, and a fix for cancelling captures in Web Workers (#331–#333). Captures that use none of the new options are byte-identical to 2.0.0. Root exports grow from 1,668 to 1,677 symbols; none is removed.
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`createDiffractionLimitedPsfProfile`.** A sampled lens PSF profile (`LensSampledPsfProfile`) for an ideal, aberration-free, in-focus lens with a circular pupil: one unit-sum Airy kernel per wavelength on one shared grid. The engine integrates each cell from the circular-pupil OTF, reports the tail energy outside the window per wavelength, and renormalizes. Scalar Fraunhofer approximation; see `docs/PSF_FOUNDATION.md`.
|
|
10
|
+
- **`simulateCapture` `lens.diffraction`.** Opt-in diffraction blur at the capture's resolved f-number (`"circular-aperture"`, optional `kernelRadiusSamples`). Stopping down now softens the image: at 128 × 128 the slanted-edge 10–90 % width grows from 1.49 px (no diffraction) to 1.56, 1.69 and 2.73 px at f/4, f/8 and f/16. `result.request.diffraction` holds the profile and how it was applied.
|
|
11
|
+
- **`simulateCapture` `sensor.antiAliasing`.** Opt-in optical low-pass (anti-aliasing) filter: a birefringent `"four-spot"` or `"two-spot"` point splitter (default separation one pixel, first MTF zero at Nyquist) or caller `"point-splitting"` components, passed to the engine's existing point-splitting optical-stack kernel. On a 128 × 128 Siemens star at f/8 it halves the false colour (100.8 → 52.2 bilinear, 68.7 → 25.1 AHD) and widens the slanted edge from 1.49 to 2.34 px. It multiplies the scene queries by the number of spots; with `lens.diffraction` the object-space convolution still applies (one convolution per distinct sub-site shift, tested against the per-tap quadrature to 1e-12).
|
|
12
|
+
- **`simulateCapture` per-run cost diagnostics.** `diagnostics.options` lists the opt-ins that changed the work of a run (new constant `SIMULATE_CAPTURE_COST_OPTIONS`, type `SimulateCaptureCostOption`), and `diagnostics.timingsMs` gains `diffraction`, `demosaic` and `encode` where those stages ran, so an application can measure each option's cost on the user's device. Additive; timings never enter a digest or output byte.
|
|
13
|
+
- **Cost of each `simulateCapture` option** (docs/QUICK_START.md). Measured wall time of every opt-in that affects run time at 48 × 48 and 128 × 128 relative to the default, with method, machine and what each buys: for example anti-aliasing 2.0×, pupil sampling 2 × 8 rays 13×, auto white balance 1.9×, output formats about 1×.
|
|
14
|
+
- **`simulateCapture` `output.demosaic: "ahd"`.** Adaptive homogeneity-directed demosaicing (`createAhdDemosaicTask`) in the facade's development, which roughly halves false colour on sharp neutral chart edges.
|
|
15
|
+
|
|
16
|
+
### Performance
|
|
17
|
+
|
|
18
|
+
- **Diffraction as an object-space convolution.** For the facade's still, distortion-free planar scene, `lens.diffraction` convolves the scene radiance once per spectral node instead of querying the scene once per PSF tap. It gives the same expected signal as the per-tap environment PSF quadrature (to 1e-12 per site, tested) and takes 0.56 s instead of 13.1 s for a 48 × 48 capture at f/8 (4-vCPU Xeon, Node 24).
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **`simulateCapture` cancellation in Web Workers.** 2.0.0 yielded to the host with `scheduler.yield()` where available. Its continuations run ahead of ordinary queued tasks (WICG Prioritized Task Scheduling, continuation priorities), so a worker's cancel message waited until the whole capture finished. The facade now yields with a `MessageChannel` message, else `setImmediate`, else `setTimeout`; queued tasks, including the caller's abort, run at the next yield, and the 4 ms timer clamp is still avoided (`src/facade/host-yield.ts`). Output is unchanged. Chromium 141 worker, 128 px capture cancelled after 100 ms, three rounds: 2.0.0 ignored the cancel and finished (2.12–2.34 s), now it aborts after one tile and the next 16 px request returns 0.15–0.20 s after the cancel; an uncancelled 128 px capture takes the same time (medians 2.17 s before, 2.16 s after, six alternating rounds).
|
|
23
|
+
|
|
24
|
+
## 2.0.0 — published October 17, 2026
|
|
4
25
|
|
|
5
26
|
A major release.
|
|
6
27
|
|
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.1.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.1.0 release record](https://github.com/Photivra/photivra/blob/main/docs/RELEASE_2_1_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.1.0
|
|
39
39
|
```
|
|
40
40
|
|
|
41
41
|
Simulate a photograph of a test chart with a generic camera, then read the developed sRGB pixels:
|
|
@@ -75,7 +75,7 @@ console.log(horizontal.provenance);
|
|
|
75
75
|
|
|
76
76
|
Start with the [Developer Guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Then use the [Usage Guide](https://github.com/Photivra/photivra/blob/main/docs/USAGE.md) and the [API reference](https://github.com/Photivra/photivra/blob/main/docs/API_REFERENCE.md), which is generated from the package exports.
|
|
77
77
|
|
|
78
|
-
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.
|
|
78
|
+
Repository `main` may describe newer, unreleased work. For the stable contract, use the docs bundled in the published package, or the exact `v2.1.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.1.0` (release candidate prepared October 17, 2026; publication is separate)
|
|
367
|
+
- Engine API contract: `2.1.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.1.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. The current release is **
|
|
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.0.0**, published on October 17, 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/dist/core/version.d.ts
CHANGED
package/dist/core/version.js
CHANGED
package/dist/core/version.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAgB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../../src/core/version.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,OAAgB,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Version of the engine release. Since 1.0.1 it equals the npm package version.\n *\n * New captures and plans record this version as their creator. Archived records keep the\n * version they were made with. Proof-of-concept (POC), schema and model contracts have their\n * own versions. Release checks confirm it matches package.json and the packed package.\n * @see docs/RELEASE_1_0_1.md\n */\nexport const ENGINE_API_VERSION = \"2.1.0\" as const;\n"]}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { ResolveLensSampledPsfInput } from "../optics/lens-psf-profile.js";
|
|
2
|
+
import type { PlanarSceneRadianceProvider } from "./scene-radiance.js";
|
|
3
|
+
export interface DiffractedPlanarProviderInput {
|
|
4
|
+
base: PlanarSceneRadianceProvider;
|
|
5
|
+
/** The optical state of the sampled PSF, as for `resolveLensSampledPsf`, without field point and wavelength. */
|
|
6
|
+
psf: Omit<ResolveLensSampledPsfInput, "fieldPointMm" | "wavelengthNm">;
|
|
7
|
+
/** Spectral nodes the environment queries (the profile's wavelength nodes). */
|
|
8
|
+
wavelengthsNm: readonly number[];
|
|
9
|
+
kernelRadius: number;
|
|
10
|
+
nativeWidth: number;
|
|
11
|
+
nativeHeight: number;
|
|
12
|
+
/** Output (active) width in sites: frame units are 2/outputWidth per site. */
|
|
13
|
+
outputWidth: number;
|
|
14
|
+
sitePitchMicrometers: number;
|
|
15
|
+
/** Planar geometry of the base provider, to map query directions to plane points exactly as it does. */
|
|
16
|
+
distanceM: number;
|
|
17
|
+
metresPerFrameUnit: number;
|
|
18
|
+
/**
|
|
19
|
+
* Offsets of the query points from the site centres, in sites (+x right, +y down): the anti-aliasing split
|
|
20
|
+
* offsets with their sign reversed (the environment queries q − a). Default: one zero offset.
|
|
21
|
+
*/
|
|
22
|
+
queryOffsetsSites?: readonly {
|
|
23
|
+
x: number;
|
|
24
|
+
y: number;
|
|
25
|
+
}[];
|
|
26
|
+
/** Called with the duration in ms of each band convolution (diagnostics only; never changes values). */
|
|
27
|
+
onConvolution?: (milliseconds: number) => void;
|
|
28
|
+
}
|
|
29
|
+
/** Internal (not exported from the package): the base provider with diffraction applied as an object-space convolution. */
|
|
30
|
+
export declare function createDiffractedPlanarProvider(input: DiffractedPlanarProviderInput): PlanarSceneRadianceProvider;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Fast execution of `simulateCapture`'s `lens.diffraction` (2.1): an object-space convolution in the scene
|
|
4
|
+
* provider, instead of one scene query per PSF tap in the environment PSF quadrature.
|
|
5
|
+
*
|
|
6
|
+
* The environment quadrature computes, at each site centre q and spectral node λ,
|
|
7
|
+
* E(q, λ) = Σ_k K_λ(k)·T·L(s_k(q), λ),
|
|
8
|
+
* where s_k(q) is the tap's source point (`sensorPsfSourcePoint`), L the scene radiance at the plane point
|
|
9
|
+
* that source point sees, and T the radiance-to-irradiance factor. For the facade's planar scene seen
|
|
10
|
+
* through the ideal rectilinear projection with a still camera and no lens field mapping, field throughput or
|
|
11
|
+
* pupil rays:
|
|
12
|
+
* - T is the same at every sensor point (paraxial π/(4·N_w²), no cos⁴ term), and
|
|
13
|
+
* - with the kernel sampled on the site pitch, every source point is the centre of another site, so the plane
|
|
14
|
+
* points form one regular lattice: u = (i + ½ − W/2)·2/w, v = −(j + ½ − H/2)·2/w (frame units, native
|
|
15
|
+
* site (i, j), W×H native sites, w output width).
|
|
16
|
+
* So E(q, λ) = T·B(q, λ) with B = K_λ ⊛ L sampled on that lattice: the same sum, in a different floating-point
|
|
17
|
+
* order. B is computed once per spectral node with the engine's isoplanatic convolution
|
|
18
|
+
* (`calculateIrradianceConvolution`, kernels from `resolveLensPsfConvolutionKernels`, exact site sampling) over a
|
|
19
|
+
* grid that holds every source (the native raster plus the kernel radius on each side), in row bands. The
|
|
20
|
+
* provider then answers each site-centre query with B instead of L, and the environment request declares no
|
|
21
|
+
* PSF. Incoherent imaging is a linear superposition of intensity PSFs, so convolving radiance on the object
|
|
22
|
+
* plane and convolving irradiance on the image plane agree for a planar object and a space-invariant PSF
|
|
23
|
+
* (MIT OpenCourseWare 2.71, lecture 22, free; also see: Goodman, Introduction to Fourier Optics, 4th ed., §6.1).
|
|
24
|
+
*
|
|
25
|
+
* With an anti-aliasing point-splitting filter (2.1, `sensor.antiAliasing`) the environment queries the PSF taps
|
|
26
|
+
* around each pre-filter source point q − a (a = a split offset) instead of around q. Those taps lie on the site
|
|
27
|
+
* lattice shifted by a, so the same argument holds on each shifted lattice: B is computed once per distinct
|
|
28
|
+
* fractional shift (offsets that differ by whole sites share one lattice; the centred four-spot filter with a
|
|
29
|
+
* one-site separation needs a single half-site-shifted lattice) over the native raster widened by the largest
|
|
30
|
+
* offset, and the environment's spatial quadrature still forms the weighted split sum.
|
|
31
|
+
*
|
|
32
|
+
* A query that does not land on one of the lattices (within 1e-6 site) is refused rather than approximated: the
|
|
33
|
+
* facade uses this path only when the lattice argument above holds.
|
|
34
|
+
*/
|
|
35
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
36
|
+
import { calculateIrradianceConvolution } from "../optics/irradiance-convolution.js";
|
|
37
|
+
import { resolveLensPsfConvolutionKernels } from "../optics/lens-psf-kernels.js";
|
|
38
|
+
/** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */
|
|
39
|
+
const BAND_ROWS = 32;
|
|
40
|
+
const CACHED_BANDS = 4;
|
|
41
|
+
const LATTICE_TOLERANCE_SITES = 1e-6;
|
|
42
|
+
/** Internal (not exported from the package): the base provider with diffraction applied as an object-space convolution. */
|
|
43
|
+
export function createDiffractedPlanarProvider(input) {
|
|
44
|
+
const { base, wavelengthsNm, nativeWidth: W, nativeHeight: H, kernelRadius: R } = input;
|
|
45
|
+
const g = 2 / input.outputWidth, pitch = input.sitePitchMicrometers;
|
|
46
|
+
const index = new Map(wavelengthsNm.map((nm, i) => [nm, i]));
|
|
47
|
+
// One lattice per distinct fractional query offset; M extra sites per side hold the offset queries.
|
|
48
|
+
const offsets = input.queryOffsetsSites ?? [{ x: 0, y: 0 }];
|
|
49
|
+
const fraction = (value) => { const f = value - Math.floor(value); return f > 1 - LATTICE_TOLERANCE_SITES ? 0 : f < LATTICE_TOLERANCE_SITES ? 0 : f; };
|
|
50
|
+
const shifts = [];
|
|
51
|
+
for (const o of offsets) {
|
|
52
|
+
const f = { x: fraction(o.x), y: fraction(o.y) };
|
|
53
|
+
if (!shifts.some((s) => Math.abs(s.x - f.x) <= LATTICE_TOLERANCE_SITES && Math.abs(s.y - f.y) <= LATTICE_TOLERANCE_SITES))
|
|
54
|
+
shifts.push(f);
|
|
55
|
+
}
|
|
56
|
+
const M = Math.max(0, ...offsets.map((o) => Math.ceil(Math.max(Math.abs(o.x), Math.abs(o.y)) - LATTICE_TOLERANCE_SITES)));
|
|
57
|
+
const LW = W + 2 * M, LH = H + 2 * M;
|
|
58
|
+
// One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0).
|
|
59
|
+
const kernels = wavelengthsNm.map((wavelengthNm) => {
|
|
60
|
+
const k = resolveLensPsfConvolutionKernels({ schemaVersion: "0.1.0", psf: input.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],
|
|
61
|
+
grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,
|
|
62
|
+
kernelRadius: R }).value;
|
|
63
|
+
if (!k.exactSiteSampling || k.kernelRadius !== R)
|
|
64
|
+
throw new InvalidConfigurationError("Diffraction kernels must sample the site pitch exactly.");
|
|
65
|
+
return k.kernels;
|
|
66
|
+
});
|
|
67
|
+
const gridWidth = LW + 2 * R;
|
|
68
|
+
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
69
|
+
const bands = new Map();
|
|
70
|
+
function band(b) {
|
|
71
|
+
const cached = bands.get(b);
|
|
72
|
+
if (cached) {
|
|
73
|
+
bands.delete(b);
|
|
74
|
+
bands.set(b, cached);
|
|
75
|
+
return cached;
|
|
76
|
+
}
|
|
77
|
+
const started = input.onConvolution ? now() : 0;
|
|
78
|
+
const y0 = b * BAND_ROWS, rows = Math.min(BAND_ROWS, LH - y0), gridHeight = rows + 2 * R;
|
|
79
|
+
const values = shifts.map((shift) => wavelengthsNm.map((nm, w) => {
|
|
80
|
+
const samples = new Float64Array(gridWidth * gridHeight);
|
|
81
|
+
for (let gy = 0; gy < gridHeight; gy++) {
|
|
82
|
+
const v = shift.y === 0 ? -(y0 + gy - R - M + 0.5 - H / 2) * g : -(y0 + gy - R - M + 0.5 + shift.y - H / 2) * g;
|
|
83
|
+
for (let gx = 0; gx < gridWidth; gx++)
|
|
84
|
+
samples[gy * gridWidth + gx] = base.radianceAt(shift.x === 0 ? (gx - R - M + 0.5 - W / 2) * g : (gx - R - M + 0.5 + shift.x - W / 2) * g, v, nm);
|
|
85
|
+
}
|
|
86
|
+
const blurred = calculateIrradianceConvolution({ schemaVersion: "0.1.0", width: gridWidth, height: gridHeight, samples, kernelRadius: R,
|
|
87
|
+
patchWidth: gridWidth, patchHeight: gridHeight, kernels: kernels[w], boundary: "zero-outside-grid" }).values;
|
|
88
|
+
const out = new Float64Array(LW * rows);
|
|
89
|
+
for (let y = 0; y < rows; y++)
|
|
90
|
+
out.set(blurred.subarray((y + R) * gridWidth + R, (y + R) * gridWidth + R + LW), y * LW);
|
|
91
|
+
return out;
|
|
92
|
+
}));
|
|
93
|
+
bands.set(b, values);
|
|
94
|
+
if (bands.size > CACHED_BANDS)
|
|
95
|
+
bands.delete(bands.keys().next().value);
|
|
96
|
+
if (input.onConvolution)
|
|
97
|
+
input.onConvolution(now() - started);
|
|
98
|
+
return values;
|
|
99
|
+
}
|
|
100
|
+
function blurredAt(u, v, nm) {
|
|
101
|
+
const w = index.get(nm), fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;
|
|
102
|
+
if (w !== undefined)
|
|
103
|
+
for (let s = 0; s < shifts.length; s++) {
|
|
104
|
+
const shift = shifts[s], i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);
|
|
105
|
+
if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES)
|
|
106
|
+
continue;
|
|
107
|
+
if (i < -M || j < -M || i >= W + M || j >= H + M)
|
|
108
|
+
break;
|
|
109
|
+
const lj = j + M;
|
|
110
|
+
return band(Math.floor(lj / BAND_ROWS))[s][w][(lj % BAND_ROWS) * LW + i + M];
|
|
111
|
+
}
|
|
112
|
+
throw new InvalidConfigurationError("Diffraction query is off the site lattice; the object-space convolution does not apply.");
|
|
113
|
+
}
|
|
114
|
+
const scale = input.metresPerFrameUnit, distance = input.distanceM;
|
|
115
|
+
return {
|
|
116
|
+
sceneId: base.sceneId, providerProfileId: base.providerProfileId, radianceAt: base.radianceAt,
|
|
117
|
+
evaluateRadiance(request) {
|
|
118
|
+
const result = base.evaluateRadiance(request), o = request.target.outgoingDirectionUnitVector;
|
|
119
|
+
const t = distance / -o.z;
|
|
120
|
+
return { ...result, spectralRadianceWattsPerSquareMeterSteradianNanometer: blurredAt(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers) };
|
|
121
|
+
},
|
|
122
|
+
evaluateApertureRadiance() {
|
|
123
|
+
throw new InvalidConfigurationError("Diffraction by object-space convolution does not take pupil rays.");
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=diffraction-provider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diffraction-provider.js","sourceRoot":"","sources":["../../src/facade/diffraction-provider.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Fast execution of `simulateCapture`'s `lens.diffraction` (2.1): an object-space convolution in the scene\n * provider, instead of one scene query per PSF tap in the environment PSF quadrature.\n *\n * The environment quadrature computes, at each site centre q and spectral node λ,\n * E(q, λ) = Σ_k K_λ(k)·T·L(s_k(q), λ),\n * where s_k(q) is the tap's source point (`sensorPsfSourcePoint`), L the scene radiance at the plane point\n * that source point sees, and T the radiance-to-irradiance factor. For the facade's planar scene seen\n * through the ideal rectilinear projection with a still camera and no lens field mapping, field throughput or\n * pupil rays:\n * - T is the same at every sensor point (paraxial π/(4·N_w²), no cos⁴ term), and\n * - with the kernel sampled on the site pitch, every source point is the centre of another site, so the plane\n * points form one regular lattice: u = (i + ½ − W/2)·2/w, v = −(j + ½ − H/2)·2/w (frame units, native\n * site (i, j), W×H native sites, w output width).\n * So E(q, λ) = T·B(q, λ) with B = K_λ ⊛ L sampled on that lattice: the same sum, in a different floating-point\n * order. B is computed once per spectral node with the engine's isoplanatic convolution\n * (`calculateIrradianceConvolution`, kernels from `resolveLensPsfConvolutionKernels`, exact site sampling) over a\n * grid that holds every source (the native raster plus the kernel radius on each side), in row bands. The\n * provider then answers each site-centre query with B instead of L, and the environment request declares no\n * PSF. Incoherent imaging is a linear superposition of intensity PSFs, so convolving radiance on the object\n * plane and convolving irradiance on the image plane agree for a planar object and a space-invariant PSF\n * (MIT OpenCourseWare 2.71, lecture 22, free; also see: Goodman, Introduction to Fourier Optics, 4th ed., §6.1).\n *\n * With an anti-aliasing point-splitting filter (2.1, `sensor.antiAliasing`) the environment queries the PSF taps\n * around each pre-filter source point q − a (a = a split offset) instead of around q. Those taps lie on the site\n * lattice shifted by a, so the same argument holds on each shifted lattice: B is computed once per distinct\n * fractional shift (offsets that differ by whole sites share one lattice; the centred four-spot filter with a\n * one-site separation needs a single half-site-shifted lattice) over the native raster widened by the largest\n * offset, and the environment's spatial quadrature still forms the weighted split sum.\n *\n * A query that does not land on one of the lattices (within 1e-6 site) is refused rather than approximated: the\n * facade uses this path only when the lattice argument above holds.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { SceneRadianceEvaluationRequest, SceneRadianceEvaluationResult } from \"../schema/scene-radiance.js\";\nimport { calculateIrradianceConvolution } from \"../optics/irradiance-convolution.js\";\nimport { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport type { ResolveLensSampledPsfInput } from \"../optics/lens-psf-profile.js\";\nimport type { PlanarSceneRadianceProvider } from \"./scene-radiance.js\";\n\n/** Rows of native sites per convolution band, and the bands kept. Banding changes memory only, never values. */\nconst BAND_ROWS = 32;\nconst CACHED_BANDS = 4;\nconst LATTICE_TOLERANCE_SITES = 1e-6;\n\nexport interface DiffractedPlanarProviderInput {\n base: PlanarSceneRadianceProvider;\n /** The optical state of the sampled PSF, as for `resolveLensSampledPsf`, without field point and wavelength. */\n psf: Omit<ResolveLensSampledPsfInput, \"fieldPointMm\" | \"wavelengthNm\">;\n /** Spectral nodes the environment queries (the profile's wavelength nodes). */\n wavelengthsNm: readonly number[];\n kernelRadius: number;\n nativeWidth: number;\n nativeHeight: number;\n /** Output (active) width in sites: frame units are 2/outputWidth per site. */\n outputWidth: number;\n sitePitchMicrometers: number;\n /** Planar geometry of the base provider, to map query directions to plane points exactly as it does. */\n distanceM: number;\n metresPerFrameUnit: number;\n /**\n * Offsets of the query points from the site centres, in sites (+x right, +y down): the anti-aliasing split\n * offsets with their sign reversed (the environment queries q − a). Default: one zero offset.\n */\n queryOffsetsSites?: readonly { x: number; y: number }[];\n /** Called with the duration in ms of each band convolution (diagnostics only; never changes values). */\n onConvolution?: (milliseconds: number) => void;\n}\n\n/** Internal (not exported from the package): the base provider with diffraction applied as an object-space convolution. */\nexport function createDiffractedPlanarProvider(input: DiffractedPlanarProviderInput): PlanarSceneRadianceProvider {\n const { base, wavelengthsNm, nativeWidth: W, nativeHeight: H, kernelRadius: R } = input;\n const g = 2 / input.outputWidth, pitch = input.sitePitchMicrometers;\n const index = new Map(wavelengthsNm.map((nm, i) => [nm, i]));\n // One lattice per distinct fractional query offset; M extra sites per side hold the offset queries.\n const offsets = input.queryOffsetsSites ?? [{ x: 0, y: 0 }];\n const fraction = (value: number): number => { const f = value - Math.floor(value); return f > 1 - LATTICE_TOLERANCE_SITES ? 0 : f < LATTICE_TOLERANCE_SITES ? 0 : f; };\n const shifts: { x: number; y: number }[] = [];\n for (const o of offsets) {\n const f = { x: fraction(o.x), y: fraction(o.y) };\n if (!shifts.some((s) => Math.abs(s.x - f.x) <= LATTICE_TOLERANCE_SITES && Math.abs(s.y - f.y) <= LATTICE_TOLERANCE_SITES)) shifts.push(f);\n }\n const M = Math.max(0, ...offsets.map((o) => Math.ceil(Math.max(Math.abs(o.x), Math.abs(o.y)) - LATTICE_TOLERANCE_SITES)));\n const LW = W + 2 * M, LH = H + 2 * M;\n // One kernel per wavelength, on the site grid, from the engine's own tap-to-site mapping (field point 0, 0).\n const kernels = wavelengthsNm.map((wavelengthNm) => {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf: input.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: R }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== R) throw new InvalidConfigurationError(\"Diffraction kernels must sample the site pitch exactly.\");\n return k.kernels;\n });\n const gridWidth = LW + 2 * R;\n const now = (): number => globalThis.performance?.now() ?? Date.now();\n const bands = new Map<number, Float64Array[][]>();\n function band(b: number): Float64Array[][] {\n const cached = bands.get(b);\n if (cached) { bands.delete(b); bands.set(b, cached); return cached; }\n const started = input.onConvolution ? now() : 0;\n const y0 = b * BAND_ROWS, rows = Math.min(BAND_ROWS, LH - y0), gridHeight = rows + 2 * R;\n const values = shifts.map((shift) => wavelengthsNm.map((nm, w) => {\n const samples = new Float64Array(gridWidth * gridHeight);\n for (let gy = 0; gy < gridHeight; gy++) {\n const v = shift.y === 0 ? -(y0 + gy - R - M + 0.5 - H / 2) * g : -(y0 + gy - R - M + 0.5 + shift.y - H / 2) * g;\n for (let gx = 0; gx < gridWidth; gx++) samples[gy * gridWidth + gx] = base.radianceAt(shift.x === 0 ? (gx - R - M + 0.5 - W / 2) * g : (gx - R - M + 0.5 + shift.x - W / 2) * g, v, nm);\n }\n const blurred = calculateIrradianceConvolution({ schemaVersion: \"0.1.0\", width: gridWidth, height: gridHeight, samples, kernelRadius: R,\n patchWidth: gridWidth, patchHeight: gridHeight, kernels: kernels[w]!, boundary: \"zero-outside-grid\" }).values;\n const out = new Float64Array(LW * rows);\n for (let y = 0; y < rows; y++) out.set(blurred.subarray((y + R) * gridWidth + R, (y + R) * gridWidth + R + LW), y * LW);\n return out;\n }));\n bands.set(b, values);\n if (bands.size > CACHED_BANDS) bands.delete(bands.keys().next().value!);\n if (input.onConvolution) input.onConvolution(now() - started);\n return values;\n }\n function blurredAt(u: number, v: number, nm: number): number {\n const w = index.get(nm), fi = u / g - 0.5 + W / 2, fj = -v / g - 0.5 + H / 2;\n if (w !== undefined) for (let s = 0; s < shifts.length; s++) {\n const shift = shifts[s]!, i = Math.round(fi - shift.x), j = Math.round(fj - shift.y);\n if (Math.abs(fi - shift.x - i) > LATTICE_TOLERANCE_SITES || Math.abs(fj - shift.y - j) > LATTICE_TOLERANCE_SITES) continue;\n if (i < -M || j < -M || i >= W + M || j >= H + M) break;\n const lj = j + M;\n return band(Math.floor(lj / BAND_ROWS))[s]![w]![(lj % BAND_ROWS) * LW + i + M]!;\n }\n throw new InvalidConfigurationError(\"Diffraction query is off the site lattice; the object-space convolution does not apply.\");\n }\n const scale = input.metresPerFrameUnit, distance = input.distanceM;\n return {\n sceneId: base.sceneId, providerProfileId: base.providerProfileId, radianceAt: base.radianceAt,\n evaluateRadiance(request: Readonly<SceneRadianceEvaluationRequest>): SceneRadianceEvaluationResult {\n const result = base.evaluateRadiance(request), o = (request.target as { outgoingDirectionUnitVector: { x: number; y: number; z: number } }).outgoingDirectionUnitVector;\n const t = distance / -o.z;\n return { ...result, spectralRadianceWattsPerSquareMeterSteradianNanometer: blurredAt(-o.x * t / scale, -o.y * t / scale, request.wavelengthNanometers) };\n },\n evaluateApertureRadiance(): SceneRadianceEvaluationResult {\n throw new InvalidConfigurationError(\"Diffraction by object-space convolution does not take pupil rays.\");\n }\n };\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Mechanism chosen by {@link createHostYield}, for diagnostics and tests. */
|
|
2
|
-
export type HostYieldMechanism = "
|
|
2
|
+
export type HostYieldMechanism = "message-channel" | "set-immediate" | "set-timeout";
|
|
3
3
|
/** Returns a function that yields to the host event loop once per call, and the mechanism it uses. */
|
|
4
4
|
export declare function createHostYield(host?: object): {
|
|
5
5
|
mechanism: HostYieldMechanism;
|
|
@@ -2,10 +2,6 @@
|
|
|
2
2
|
/** Returns a function that yields to the host event loop once per call, and the mechanism it uses. */
|
|
3
3
|
export function createHostYield(host = globalThis) {
|
|
4
4
|
const g = host;
|
|
5
|
-
const scheduler = g.scheduler;
|
|
6
|
-
if (scheduler !== undefined && typeof scheduler.yield === "function") {
|
|
7
|
-
return { mechanism: "scheduler.yield", yieldControl: async () => { await scheduler.yield.call(scheduler); } };
|
|
8
|
-
}
|
|
9
5
|
const Channel = g.MessageChannel;
|
|
10
6
|
if (typeof Channel === "function") {
|
|
11
7
|
return { mechanism: "message-channel", yieldControl: () => new Promise((resolve) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"host-yield.js","sourceRoot":"","sources":["../../src/facade/host-yield.ts"],"names":[],"mappings":"AAAA,sCAAsC;
|
|
1
|
+
{"version":3,"file":"host-yield.js","sourceRoot":"","sources":["../../src/facade/host-yield.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAsCtC,sGAAsG;AACtG,MAAM,UAAU,eAAe,CAAC,OAAe,UAAU;IACvD,MAAM,CAAC,GAAG,IAAoB,CAAC;IAC/B,MAAM,OAAO,GAAG,CAAC,CAAC,cAAc,CAAC;IACjC,IAAI,OAAO,OAAO,KAAK,UAAU,EAAE,CAAC;QAClC,OAAO,EAAE,SAAS,EAAE,iBAAiB,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;gBACvF,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;gBAC9B,OAAO,CAAC,KAAK,CAAC,SAAS,GAAG,GAAS,EAAE,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC5E,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;YACvC,CAAC,CAAC,EAAE,CAAC;IACP,CAAC;IACD,MAAM,SAAS,GAAG,CAAC,CAAC,YAAY,CAAC;IACjC,IAAI,OAAO,SAAS,KAAK,UAAU,EAAE,CAAC;QACpC,OAAO,EAAE,SAAS,EAAE,eAAe,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,GAAG,SAAS,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAChI,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,YAAY,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AACvH,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Host yield for the capture facade's cooperative tasks. Browsers clamp nested `setTimeout(…, 0)` to\n * at least 4 ms (WHATWG HTML timers, nesting level above 5), so a task that yields every few\n * milliseconds through timers can sit idle for a large share of its run time. The first available\n * mechanism is used:\n *\n * 1. a `MessageChannel` message, a new task without the timer clamp (one channel per yield, closed after\n * delivery, so no port keeps a Node process alive);\n * 2. `setImmediate` (Node and some other non-browser hosts);\n * 3. `setTimeout(…, 0)` as the last resort.\n *\n * Every yield must let tasks that are already queued run first, so that a caller's cancel message (for\n * example a Web Worker `message` event that aborts the capture's `AbortSignal`) is handled at the next\n * yield. A `MessageChannel` message is queued on the HTML posted message task source, the same source as\n * worker and port `message` events, and tasks from one source run in the order they were queued.\n *\n * `scheduler.yield()` is deliberately not used (engine 2.0.0 used it first). Its continuation inherits\n * the \"user-visible\" priority and gets a higher effective priority than ordinary tasks; the WICG\n * Prioritized Task Scheduling draft (§ \"Task and Continuation Priorities\", effective priority table;\n * § \"Event loop: processing model\" note) intends such continuations to run ahead of most other task\n * sources, and its explainer (\"Preventing Task Starvation by Continuations\") only bounds that by\n * duration. A capture that yields every 8 ms through `scheduler.yield()` therefore starved a worker's\n * cancel message until the capture finished (measured in Chromium).\n *\n * Only global feature detection is used; nothing is imported, so the browser package surface is unchanged.\n * The yield changes scheduling only, never results.\n */\n\ntype YieldGlobals = {\n MessageChannel?: new () => { port1: { onmessage: ((event: unknown) => void) | null; close(): void }; port2: { postMessage(message: unknown): void } };\n setImmediate?: (callback: () => void) => unknown;\n};\n\n/** Mechanism chosen by {@link createHostYield}, for diagnostics and tests. */\nexport type HostYieldMechanism = \"message-channel\" | \"set-immediate\" | \"set-timeout\";\n\n/** Returns a function that yields to the host event loop once per call, and the mechanism it uses. */\nexport function createHostYield(host: object = globalThis): { mechanism: HostYieldMechanism; yieldControl: () => Promise<void> } {\n const g = host as YieldGlobals;\n const Channel = g.MessageChannel;\n if (typeof Channel === \"function\") {\n return { mechanism: \"message-channel\", yieldControl: () => new Promise<void>((resolve) => {\n const channel = new Channel();\n channel.port1.onmessage = (): void => { channel.port1.close(); resolve(); };\n channel.port2.postMessage(undefined);\n }) };\n }\n const immediate = g.setImmediate;\n if (typeof immediate === \"function\") {\n return { mechanism: \"set-immediate\", yieldControl: () => new Promise<void>((resolve) => { immediate.call(host, resolve); }) };\n }\n return { mechanism: \"set-timeout\", yieldControl: () => new Promise<void>((resolve) => { setTimeout(resolve, 0); }) };\n}\n"]}
|
|
@@ -12,6 +12,8 @@ import type { RadialDistortionProfile } from "../optics/radial-distortion.js";
|
|
|
12
12
|
import type { BrownConradyDistortionProfile } from "../optics/brown-conrady-distortion.js";
|
|
13
13
|
import type { IlluminationVignettingProfile } from "../optics/illumination-vignetting.js";
|
|
14
14
|
import type { CalculateEnvironmentSensorPhotoSignalInput } from "../sensor/environment-photo-signal.js";
|
|
15
|
+
import { type DiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
16
|
+
import { type AntiAliasingPointSplitComponent } from "../sensor/optical-stack.js";
|
|
15
17
|
import { type ChartScene } from "./chart-scenes.js";
|
|
16
18
|
import { type CaptureIlluminant, type CallerRadianceScene, type CallerReflectanceScene } from "./scene-radiance.js";
|
|
17
19
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
@@ -38,8 +40,12 @@ export type SimulateCaptureWhiteBalance = "scene-illuminant" | "auto" | "none" |
|
|
|
38
40
|
* PQ and HLG (BT.2100) are not part of 2.0; see THIRD_PARTY.md.
|
|
39
41
|
*/
|
|
40
42
|
export type SimulateCaptureColorEncoding = "srgb" | "display-p3" | "rec2020";
|
|
41
|
-
/**
|
|
42
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Demosaic. `bilinear`: the built-in Bayer kernels (`createBuiltInBayerDemosaicProfiles`). `ahd` (2.1): adaptive
|
|
45
|
+
* homogeneity-directed demosaicing (`createAhdDemosaicTask`, after Hirakawa & Parks 2005), a non-linear
|
|
46
|
+
* edge-directed method that reduces zipper and false colour on sharp edges.
|
|
47
|
+
*/
|
|
48
|
+
export type SimulateCaptureDemosaic = "bilinear" | "ahd";
|
|
43
49
|
/**
|
|
44
50
|
* `margin` renders extra native sites around the output, as far as the kernel reaches. `mirror` renders
|
|
45
51
|
* exactly the output and reads mirrored (reflect-101) copies of the missing border sites. Mirroring is
|
|
@@ -53,6 +59,32 @@ export type SimulateCaptureLensDistortion = {
|
|
|
53
59
|
kind: "brown-conrady";
|
|
54
60
|
profile: BrownConradyDistortionProfile;
|
|
55
61
|
};
|
|
62
|
+
/** Diffraction model (2.1): the shorthand or a record with an explicit kernel radius in sites. */
|
|
63
|
+
export type SimulateCaptureLensDiffraction = "circular-aperture" | {
|
|
64
|
+
aperture: "circular-aperture";
|
|
65
|
+
kernelRadiusSamples?: number;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Optical anti-aliasing (low-pass) filter (2.1), modelled as birefringent point splitting: each image point is
|
|
69
|
+
* split into equally bright copies (the engine's `normalized-point-splitting-kernel` optical-stack response).
|
|
70
|
+
* - `four-spot`: two crossed birefringent plates; copies at (±d/2, ±d/2), weight 1/4 each, MTF |cos(π·f_x·d)·cos(π·f_y·d)|.
|
|
71
|
+
* - `two-spot`: one plate splitting along `axis`; copies at ±d/2, weight 1/2 each, MTF |cos(π·f·d)|.
|
|
72
|
+
* - `point-splitting`: the engine contract itself, offsets in micrometres (+x right, +y down) and positive weights
|
|
73
|
+
* summing to 1 (at most 16 components, each within 4 sites).
|
|
74
|
+
* d = `separationSites` × pixel pitch, 0 < separationSites ≤ 4, default 1 (first MTF zero at the sensor's Nyquist
|
|
75
|
+
* frequency 1/(2·pitch)).
|
|
76
|
+
*/
|
|
77
|
+
export type SimulateCaptureSensorAntiAliasing = {
|
|
78
|
+
kind: "four-spot";
|
|
79
|
+
separationSites?: number;
|
|
80
|
+
} | {
|
|
81
|
+
kind: "two-spot";
|
|
82
|
+
axis: "horizontal" | "vertical";
|
|
83
|
+
separationSites?: number;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "point-splitting";
|
|
86
|
+
components: readonly AntiAliasingPointSplitComponent[];
|
|
87
|
+
};
|
|
56
88
|
export interface SimulateCaptureMotion {
|
|
57
89
|
/** Constant camera angular velocity, rad/s, in the engine's pitch/yaw/roll convention. Missing axes are 0. */
|
|
58
90
|
angularVelocityRadPerSec?: {
|
|
@@ -93,6 +125,13 @@ export interface SimulateCaptureOptions {
|
|
|
93
125
|
psf?: Extract<CalculateEnvironmentSensorPhotoSignalInput["psf"], {
|
|
94
126
|
kind: "sampled-local";
|
|
95
127
|
}>;
|
|
128
|
+
/**
|
|
129
|
+
* Opt-in (2.1) diffraction blur of an ideal, aberration-free, in-focus lens with a circular pupil at the
|
|
130
|
+
* capture's resolved f-number: the engine builds the Airy kernels (`createDiffractionLimitedPsfProfile`)
|
|
131
|
+
* on the site pitch. `kernelRadiusSamples` (0–32) sets the kernel window; default: the third dark ring at
|
|
132
|
+
* the longest spectral node. Not combinable with `psf` or `pupilSampling`. Default off.
|
|
133
|
+
*/
|
|
134
|
+
diffraction?: SimulateCaptureLensDiffraction;
|
|
96
135
|
};
|
|
97
136
|
exposure?: {
|
|
98
137
|
/** Default "manual". */
|
|
@@ -157,12 +196,23 @@ export interface SimulateCaptureOptions {
|
|
|
157
196
|
temporalSampleCount?: number;
|
|
158
197
|
/** Noise seed (uint32). Default 1. */
|
|
159
198
|
noiseSeed?: number;
|
|
199
|
+
/**
|
|
200
|
+
* Opt-in (2.1) optical anti-aliasing filter (birefringent point splitting, `SimulateCaptureSensorAntiAliasing`).
|
|
201
|
+
* Multiplies the scene queries by the number of split points. Default off (the tiers declare no filter).
|
|
202
|
+
*/
|
|
203
|
+
antiAliasing?: SimulateCaptureSensorAntiAliasing;
|
|
160
204
|
};
|
|
161
205
|
/** Aborting cancels the running task; the promise rejects with a domain error and returns no partial image. */
|
|
162
206
|
signal?: AbortSignal;
|
|
163
207
|
/** Called after each committed RAW, development or JPEG tile. Cannot change output. */
|
|
164
208
|
onProgress?: (progress: SimulateCaptureProgress) => void;
|
|
165
209
|
}
|
|
210
|
+
/**
|
|
211
|
+
* Options reported in `diagnostics.options` (2.1), in this order, when they ran: the ones that change how much work
|
|
212
|
+
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
213
|
+
*/
|
|
214
|
+
export declare const SIMULATE_CAPTURE_COST_OPTIONS: readonly ["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf", "lens.diffraction", "sensor.antiAliasing", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual", "output.format.jpeg", "output.format.png", "output.format.dng"];
|
|
215
|
+
export type SimulateCaptureCostOption = typeof SIMULATE_CAPTURE_COST_OPTIONS[number];
|
|
166
216
|
export interface SimulateCaptureProgress {
|
|
167
217
|
stage: "raw" | "white-balance" | "development" | "jpeg";
|
|
168
218
|
completedTileCount: number;
|
|
@@ -204,6 +254,19 @@ export interface SimulateCaptureResult {
|
|
|
204
254
|
siteInput(nativeIndex: number): SimulateEnvironmentSensorRawFrameInput["sites"][number];
|
|
205
255
|
development: Omit<NativeRawDevelopmentInput, "raw">;
|
|
206
256
|
whiteBalanceState: ResolvedWhiteBalanceState | null;
|
|
257
|
+
/**
|
|
258
|
+
* Present with lens.diffraction (2.1): the engine-built Airy profile, the equivalent environment sampled PSF
|
|
259
|
+
* selection, and how it was applied. With `object-space-convolution` the site inputs declare no PSF and the
|
|
260
|
+
* provider answers with the PSF-convolved radiance; putting `psf` into each site input and using the plain
|
|
261
|
+
* scene provider gives the same expected signal (to float rounding) through the per-tap environment quadrature.
|
|
262
|
+
*/
|
|
263
|
+
diffraction?: {
|
|
264
|
+
profile: DiffractionLimitedPsfProfile;
|
|
265
|
+
psf: Extract<CalculateEnvironmentSensorPhotoSignalInput["psf"], {
|
|
266
|
+
kind: "sampled-local";
|
|
267
|
+
}>;
|
|
268
|
+
execution: "object-space-convolution" | "environment-psf-quadrature";
|
|
269
|
+
};
|
|
207
270
|
};
|
|
208
271
|
exposure: {
|
|
209
272
|
mode: SimulateCaptureExposureMode;
|
|
@@ -250,13 +313,29 @@ export interface SimulateCaptureResult {
|
|
|
250
313
|
adjustedPixelCount: number;
|
|
251
314
|
lightnessClippedPixelCount: number;
|
|
252
315
|
};
|
|
316
|
+
/**
|
|
317
|
+
* Wall-clock milliseconds per stage. `raw`, `whiteBalance` (the second RAW pass of auto white balance), `development`
|
|
318
|
+
* (includes DNG writing), `jpeg` and `total` are whole milliseconds (`Date.now()`). Since 2.1, where a stage ran:
|
|
319
|
+
* `diffraction` (the object-space convolution of `lens.diffraction`; part of `raw` and `whiteBalance`), `demosaic`
|
|
320
|
+
* (the AHD demosaic; part of `development`) and `encode` (PNG, JPEG or DNG serialization), with sub-millisecond
|
|
321
|
+
* resolution where the host has `performance.now()`. Timings vary from run to run and never enter any digest or output byte.
|
|
322
|
+
*/
|
|
253
323
|
timingsMs: {
|
|
254
324
|
raw: number;
|
|
255
325
|
whiteBalance: number;
|
|
256
326
|
development: number;
|
|
257
327
|
jpeg: number;
|
|
258
328
|
total: number;
|
|
329
|
+
diffraction?: number;
|
|
330
|
+
demosaic?: number;
|
|
331
|
+
encode?: number;
|
|
259
332
|
};
|
|
333
|
+
/**
|
|
334
|
+
* 2.1: the opt-in options that changed the work of this run, in a fixed order (see `SIMULATE_CAPTURE_COST_OPTIONS`).
|
|
335
|
+
* Empty for a capture with every default. With `timingsMs` it lets an application measure each option's cost on
|
|
336
|
+
* the user's device.
|
|
337
|
+
*/
|
|
338
|
+
options: readonly SimulateCaptureCostOption[];
|
|
260
339
|
};
|
|
261
340
|
provenance: {
|
|
262
341
|
scientificStatus: "approximation";
|