@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/docs/QUICK_START.md
CHANGED
|
@@ -50,7 +50,7 @@ the example above is [simulate-capture.mjs](examples/simulate-capture.mjs).
|
|
|
50
50
|
| `request` | The complete underlying request: `environmentRaw` (the native environment RAW input), `siteInput(index)` (rebuilds the physical input of any sensor site), `development` (the RAW development input) and `whiteBalanceState` |
|
|
51
51
|
| `exposure` | Mode, resolved settings, EV100, metered scene luminance and the exposure-mode resolver's full output |
|
|
52
52
|
| `scene` | Chart parameters, illuminant, distance, framed size and the scene's SHA-256 digest (it covers caller illuminant and reflectance spectra; see `sourceSha256`) |
|
|
53
|
-
| `diagnostics` | Radiance query count, highlight and rendering counters,
|
|
53
|
+
| `diagnostics` | Radiance query count, highlight and rendering counters, per-stage timings (`timingsMs`) and, from 2.1, the opt-ins that ran (`options`); see [Cost of each option](#cost-of-each-option) |
|
|
54
54
|
| `provenance` | Status (`approximation`), camera preset identity, evidence, the composed contracts in order, and limitations |
|
|
55
55
|
| `assumptions` | Every default, derived value and fixed modelling choice, each with an explanation |
|
|
56
56
|
|
|
@@ -71,6 +71,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
71
71
|
| `lens.distortion` | none | `{ kind: "radial", profile }` or `{ kind: "brown-conrady", profile }`. |
|
|
72
72
|
| `lens.vignetting` | none | An `IlluminationVignettingProfile` (radial relative illumination). |
|
|
73
73
|
| `lens.psf` | none | A `{ kind: "sampled-local", configuration }` sampled lens PSF, passed to the environment contract unchanged. |
|
|
74
|
+
| `lens.diffraction` | off | 2.1: `"circular-aperture"` or `{ aperture: "circular-aperture", kernelRadiusSamples }`: diffraction blur of an ideal lens at the resolved f-number (see [Optics](#optics)). |
|
|
74
75
|
| `exposure.mode` | `"manual"` | Also `"aperture-priority"`, `"shutter-priority"`, `"program"`. |
|
|
75
76
|
| `exposure.iso` | 100 | Selects the body's ISO readout regime (gain and read noise). |
|
|
76
77
|
| `exposure.shutterSeconds` | 1/125 | Used in `manual` and `shutter-priority`. |
|
|
@@ -85,7 +86,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
85
86
|
| `output.bitDepth` | 8 | PNG only: 8 or 16. |
|
|
86
87
|
| `output.gamutMapping` | `"clip"` | PNG only: or `"perceptual"`. |
|
|
87
88
|
| `output.dng.capturedAtUtc` | none | DNG only: capture time for the date tags. |
|
|
88
|
-
| `output.demosaic` | the 1.0 bilinear kernels | `"bilinear"` (see [Demosaic](#demosaic)). |
|
|
89
|
+
| `output.demosaic` | the 1.0 bilinear kernels | `"bilinear"` or, from 2.1, `"ahd"` (see [Demosaic](#demosaic)). |
|
|
89
90
|
| `output.borderHandling` | `"margin"` | Or `"mirror"`. |
|
|
90
91
|
| `output.toneCurve` | `"identity"` | Any `SdrToneCurve`, for example `"positive-reinhard-per-channel"`. |
|
|
91
92
|
| `output.toneCurveParameters` | none | Required by the luminance curves (`extended-reinhard-luminance`, `naka-rushton-luminance`); the facade invents no curve parameters. |
|
|
@@ -96,14 +97,16 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
96
97
|
| `sensor.spectralStepNanometers` | 20 | Spectral quadrature over 380–780 nm. |
|
|
97
98
|
| `sensor.temporalSampleCount` | 1, or 8 when the camera rotates | Midpoints across each site's exposure window. |
|
|
98
99
|
| `sensor.noiseSeed` | 1 | Output is deterministic for a given seed. |
|
|
100
|
+
| `sensor.antiAliasing` | off | 2.1: an optical low-pass (anti-aliasing) filter: `{ kind: "four-spot", separationSites? }`, `{ kind: "two-spot", axis, separationSites? }` or `{ kind: "point-splitting", components }` (see [Anti-aliasing filter](#anti-aliasing-filter)). |
|
|
99
101
|
| `motion.angularVelocityRadPerSec` | still | `{ pitch?, yaw?, roll? }` in rad/s (see [Motion](#motion)). |
|
|
100
102
|
| `motion.shutter` | `"global"` | Or `"rolling"`. |
|
|
101
103
|
| `motion.rollingShutterSkewSeconds` | the body's readout skew | Rolling only. |
|
|
102
|
-
| `signal`, `onProgress` | none | An `AbortSignal` cancels the running task; `onProgress` reports `raw`, `white-balance`, `development` and `jpeg` tiles. The last event of each stage has `completedTileCount === totalTileCount`. |
|
|
104
|
+
| `signal`, `onProgress` | none | An `AbortSignal` cancels the running task; `onProgress` reports `raw`, `white-balance`, `development` and `jpeg` tiles. The last event of each stage has `completedTileCount === totalTileCount`. The capture yields to the host about every 8 ms with a `MessageChannel` message (else `setImmediate`, else `setTimeout`), so other queued tasks run in between: an abort sent from another task, such as a Web Worker `message` event, takes effect at the next yield. |
|
|
103
105
|
|
|
104
106
|
Unknown fields are rejected, so a typo never silently falls back to a default.
|
|
105
107
|
|
|
106
|
-
Every option added in 2.0 (PNG, demosaic, optics and motion)
|
|
108
|
+
Every option added in 2.0 (PNG, demosaic, optics and motion) and in 2.1 (`lens.diffraction`,
|
|
109
|
+
`output.demosaic: "ahd"`, `sensor.antiAliasing`) is opt-in. With none of them given, the
|
|
107
110
|
request, RAW, samples and JPEG are byte-identical to the 1.0 facade at the same illuminance, and a
|
|
108
111
|
test checks that. (The default illuminance changed before release; see [Exposure](#exposure).)
|
|
109
112
|
|
|
@@ -217,14 +220,14 @@ These choices appear in `assumptions` as `kind: "model"` entries:
|
|
|
217
220
|
lens capability. By default no PSF (point-spread function) or diffraction, vignetting, distortion,
|
|
218
221
|
lateral colour or stray light is applied. The tier's optical data exist only at f/4 and 5 m, and
|
|
219
222
|
the facade does not extrapolate them. Vignetting, distortion and PSF are applied only from profiles
|
|
220
|
-
you supply.
|
|
223
|
+
you supply; diffraction of an ideal lens only with `lens.diffraction` (2.1).
|
|
221
224
|
- **Sensor.** Square pixels with a fill factor of 1 and no anti-aliasing filter. A global
|
|
222
225
|
electronic shutter unless `motion.shutter` is `"rolling"`. A synthetic uniform dark current of
|
|
223
226
|
1 e⁻/s at 20 °C (the tiers declare none). The charge capacity equals the body's base-ISO saturation signal.
|
|
224
227
|
- **Demosaic.** Bilinear RGGB interpolation through the existing linear-kernel contract, unless
|
|
225
|
-
`output.demosaic` selects a built-in kernel. (Demosaicing fills in the two missing colours at each
|
|
228
|
+
`output.demosaic` selects a built-in kernel or AHD. (Demosaicing fills in the two missing colours at each
|
|
226
229
|
pixel of a colour-filter mosaic.) Sharp edges without lens blur therefore show the usual zipper and
|
|
227
|
-
false-colour artefacts.
|
|
230
|
+
false-colour artefacts; `"ahd"` and `lens.diffraction` reduce them.
|
|
228
231
|
- **Scene.** A planar Lambertian surface: radiance = reflectance × irradiance / π, with illuminance
|
|
229
232
|
from K_m = 683 lm/W and the analytic ȳ. No shadows, inter-reflection, gloss or fluorescence.
|
|
230
233
|
- **Rendering.** Linear sRGB with RAW white at display white, no exposure push, out-of-gamut
|
|
@@ -323,9 +326,23 @@ console.log(dng.width, dng.activeArea, dng.whiteLevel, dng.codes.length === shot
|
|
|
323
326
|
|
|
324
327
|
## Demosaic
|
|
325
328
|
|
|
326
|
-
`output.demosaic` selects
|
|
327
|
-
|
|
328
|
-
|
|
329
|
+
`output.demosaic` selects:
|
|
330
|
+
|
|
331
|
+
- `"bilinear"`: the built-in Bayer kernels of `createBuiltInBayerDemosaicProfiles` (nearest same-channel
|
|
332
|
+
averages; pixel-identical to the default).
|
|
333
|
+
- `"ahd"` (2.1): adaptive homogeneity-directed demosaicing, `createAhdDemosaicTask` (after Hirakawa &
|
|
334
|
+
Parks, *IEEE Trans. Image Processing* 14(3), 2005). Green is interpolated along rows and along columns,
|
|
335
|
+
red and blue follow from colour differences, and each pixel takes the more homogeneous of the two
|
|
336
|
+
candidates in CIELAB. It is non-linear and edge-directed, so it shows much less zipper and false colour
|
|
337
|
+
on sharp edges than bilinear (mean false colour of a neutral 32 × 32 Siemens star: 138 instead of 209
|
|
338
|
+
8-bit codes; slanted edge: 5.3 instead of 11.4). The RAW sites are normalized and white-level clipped
|
|
339
|
+
exactly as in the native development, the homogeneity is measured on white-balanced colour, and the
|
|
340
|
+
native development's colour operator and SDR kernel follow. The paper's iterative median step is not
|
|
341
|
+
applied. With `"margin"` borders the sensor gets 5 extra sites per side (an AHD pixel depends on sites
|
|
342
|
+
up to 5 away); `request.development` keeps the bilinear kernels, which validate the development but do
|
|
343
|
+
not produce the pixels.
|
|
344
|
+
|
|
345
|
+
The Malvar–He–Cutler kernels are not offered (a patent is still in force; see
|
|
329
346
|
[PROCESSING.md](https://github.com/Photivra/photivra/blob/main/docs/PROCESSING.md)).
|
|
330
347
|
|
|
331
348
|
`output.borderHandling` decides how edge pixels get their neighbours:
|
|
@@ -365,6 +382,25 @@ The optics options feed only what the environment site contract accepts:
|
|
|
365
382
|
1 + r2·ρ² + r4·ρ⁴ + r6·ρ⁶, applied once at the physical sensor point before any PSF.
|
|
366
383
|
- `lens.psf` is a caller sampled PSF passed through unchanged. Its focal length, f-number and focus
|
|
367
384
|
must equal the resolved capture settings exactly, and it cannot be combined with pupil sampling.
|
|
385
|
+
- `lens.diffraction` (2.1) adds the diffraction blur of an ideal lens at the capture's resolved
|
|
386
|
+
f-number, so stopping down now softens the image as well as darkening it.
|
|
387
|
+
- The engine builds the kernels (`createDiffractionLimitedPsfProfile`): the Airy pattern of a uniformly
|
|
388
|
+
lit circular pupil (scalar Fraunhofer diffraction), aberration-free and in focus, with pupil diameter
|
|
389
|
+
f/N and the thin-lens image distance (working f-number N·v/f). Each kernel holds the PSF energy of
|
|
390
|
+
site-sized cells, one kernel per spectral node.
|
|
391
|
+
- The kernel window reaches the third dark ring at the longest node by default (2, 4 and 7 sites at
|
|
392
|
+
f/4, f/8 and f/16 with 6 µm pixels); set `kernelRadiusSamples` (0–32) to change it. The Airy tail
|
|
393
|
+
outside the window (2–5 % of the energy with the default) is listed per wavelength in the
|
|
394
|
+
`lens.diffraction` assumption and put back by renormalizing each kernel to unit sum.
|
|
395
|
+
- It runs as an object-space convolution: the scene radiance on the site lattice is convolved once per
|
|
396
|
+
spectral node and the provider answers each site query with it. That is the same sum as the
|
|
397
|
+
environment PSF quadrature (one scene query per kernel tap) for this still, distortion-free planar
|
|
398
|
+
scene; a test checks it to 1e-12 per site. With camera rotation, a projection or distortion, or
|
|
399
|
+
vignetting, the per-tap quadrature runs instead (much slower). `request.diffraction` holds the profile,
|
|
400
|
+
the equivalent `sampled-local` PSF and which execution ran.
|
|
401
|
+
- Not modelled: defocus, aberrations, pupil clipping, a polygonal (bladed) aperture (the tiers declare
|
|
402
|
+
no blade count) and the pixel aperture (each site is still sampled at its centre). It cannot be
|
|
403
|
+
combined with `lens.psf` or `lens.pupilSampling`.
|
|
368
404
|
|
|
369
405
|
The default frame is a 0.38 mm crop behind a 50 mm lens, a field of well under a degree. Projection
|
|
370
406
|
and distortion effects therefore only become visible with larger pixels (or a custom lens). The
|
|
@@ -397,6 +433,65 @@ const lensy = await simulateCapture({
|
|
|
397
433
|
console.log(lensy.request.siteInput(0).environment.lensFieldMapping, lensy.assumptions.find((a) => a.id === "lens.vignetting")?.kind);
|
|
398
434
|
```
|
|
399
435
|
|
|
436
|
+
```ts
|
|
437
|
+
import { simulateCapture } from "@photivra/engine";
|
|
438
|
+
|
|
439
|
+
const stoppedDown = await simulateCapture({
|
|
440
|
+
scene: { preset: "siemens-star" },
|
|
441
|
+
lens: { fNumber: 16, diffraction: "circular-aperture" },
|
|
442
|
+
exposure: { shutterSeconds: 1 / 30 },
|
|
443
|
+
output: { width: 48, height: 48, demosaic: "ahd" }
|
|
444
|
+
});
|
|
445
|
+
console.log(stoppedDown.request.diffraction?.execution, stoppedDown.request.diffraction?.profile.maximumTruncatedEnergyFraction);
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### Anti-aliasing filter
|
|
449
|
+
|
|
450
|
+
`sensor.antiAliasing` (2.1) puts an optical low-pass filter in front of the sensor. Most real cameras
|
|
451
|
+
with a Bayer sensor carry one; the generic tiers declare none, so it is off by default. It is modelled
|
|
452
|
+
the way a birefringent quartz filter works: every image point is split into equally bright copies, and
|
|
453
|
+
each site collects the light that would otherwise have landed at its centre minus each offset (the
|
|
454
|
+
engine's `normalized-point-splitting-kernel` optical-stack response, resolved by
|
|
455
|
+
`resolveAntiAliasingSpatialKernel`).
|
|
456
|
+
|
|
457
|
+
- `{ kind: "four-spot", separationSites }`: two crossed plates, four spots at (±d/2, ±d/2) with weight
|
|
458
|
+
1/4 each. Its MTF is |cos(π·f_x·d)·cos(π·f_y·d)|. The most common filter in digital cameras.
|
|
459
|
+
- `{ kind: "two-spot", axis: "horizontal" | "vertical", separationSites }`: one plate, two spots at
|
|
460
|
+
±d/2 along the axis, MTF |cos(π·f·d)|.
|
|
461
|
+
- `{ kind: "point-splitting", components }`: your own kernel, offsets in micrometres (+x right, +y
|
|
462
|
+
down) and positive weights that sum to 1; at most 16 components, each within 4 sites.
|
|
463
|
+
- d = `separationSites` × pixel pitch, 0 < `separationSites` ≤ 4. The default 1 puts the first MTF
|
|
464
|
+
zero at the sensor's Nyquist frequency 1/(2·pitch): a four-spot filter one pixel wide cuts off at
|
|
465
|
+
Nyquist (Palum 2006). Above Nyquist the cosine rises again, so a lens (for example
|
|
466
|
+
`lens.diffraction`) is still needed to suppress the highest frequencies.
|
|
467
|
+
|
|
468
|
+
Measured at f/8 on 128 × 128 neutral charts (mean false colour |R − G| + |B − G| in 8-bit codes):
|
|
469
|
+
Siemens star 100.8 → 52.2 with bilinear and 68.7 → 25.1 with AHD; slanted edge 4.9 → 3.8 (bilinear).
|
|
470
|
+
The price is sharpness: the slanted-edge 10–90 % width grows from 1.49 to 2.34 px (bilinear).
|
|
471
|
+
|
|
472
|
+
Each site queries the scene once per spot, so the radiance work grows by the number of spots (4 for
|
|
473
|
+
`four-spot`). With `lens.diffraction` the object-space convolution still applies: the taps around
|
|
474
|
+
each split point lie on the site lattice shifted by its offset, so the engine convolves once per
|
|
475
|
+
distinct sub-site shift (one half-site lattice for the default four-spot filter). A test checks it
|
|
476
|
+
against the per-tap environment quadrature to 1e-12 per site.
|
|
477
|
+
|
|
478
|
+
Approximations, listed in the `sensor.antiAliasing` assumption: the split is the same at every
|
|
479
|
+
wavelength, field point and polarization (birefringence dispersion is ignored), the spots are exact
|
|
480
|
+
points of equal weight for unpolarized light, and the filter's transmission, reflections and focus
|
|
481
|
+
shift are not modelled. It is not a measured filter.
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { simulateCapture } from "@photivra/engine";
|
|
485
|
+
|
|
486
|
+
const filtered = await simulateCapture({
|
|
487
|
+
scene: { preset: "siemens-star" },
|
|
488
|
+
sensor: { antiAliasing: { kind: "four-spot" } },
|
|
489
|
+
lens: { diffraction: "circular-aperture" },
|
|
490
|
+
output: { width: 48, height: 48, demosaic: "ahd" }
|
|
491
|
+
});
|
|
492
|
+
console.log(filtered.assumptions.find((a) => a.id === "sensor.antiAliasing")?.value);
|
|
493
|
+
```
|
|
494
|
+
|
|
400
495
|
## Motion
|
|
401
496
|
|
|
402
497
|
`motion.angularVelocityRadPerSec` rotates the camera at a constant rate during the exposure, in the
|
|
@@ -450,10 +545,78 @@ The 1000 × 1000 row is not a measurement. It multiplies the 256 × 256 CPU cost
|
|
|
450
545
|
count. Before the lean executor, the same 64 × 64 capture took about 6.0 s of CPU on this host, and
|
|
451
546
|
128 × 128 about 20.6 s. Development and encoding are a small part of the total.
|
|
452
547
|
|
|
548
|
+
The 2.1 options, measured on the same kind of host (4-vCPU Intel Xeon 2.80 GHz, Node 24, median of three
|
|
549
|
+
runs, slanted edge and Siemens star): at 48 × 48, default 0.38–0.45 s, `lens.diffraction` 0.49–0.71 s
|
|
550
|
+
(f/4–f/16), `"ahd"` 0.48–0.63 s, both 0.60–0.87 s; at 128 × 128, default 2.1–2.3 s, diffraction
|
|
551
|
+
2.5–3.4 s, AHD 2.4–2.5 s, both 2.8–4.0 s. The same diffraction through the per-tap environment quadrature
|
|
552
|
+
took 13.1 s at 48 × 48, f/8. [Cost of each option](#cost-of-each-option) covers every opt-in.
|
|
553
|
+
|
|
453
554
|
These numbers come from one run on one machine. They are not a performance claim. Small captures
|
|
454
555
|
(up to about 128 × 128) take a few seconds. Use them while you explore, and render large images only
|
|
455
556
|
when you need them.
|
|
456
557
|
|
|
558
|
+
## Cost of each option
|
|
559
|
+
|
|
560
|
+
Slow options are opt-in, so an application can decide which ones it can afford: a feature that is
|
|
561
|
+
too slow for a phone today may be cheap on the next generation of hardware. The table gives the
|
|
562
|
+
measured wall time of each opt-in that affects run time, relative to the default capture of the same
|
|
563
|
+
size.
|
|
564
|
+
|
|
565
|
+
**Method.** Slanted-edge chart, every other option at its default (f/8, 1/125 s, ISO 100, sRGB 8-bit,
|
|
566
|
+
bilinear demosaic). One fresh Node process per row and size; one 16 × 16 warm-up capture, then three
|
|
567
|
+
timed captures (`performance.now()` around `simulateCapture`), median reported. Built `dist`, Node
|
|
568
|
+
24.21.0, 4-vCPU Intel Xeon 2.80 GHz, Linux, nothing else running. One run on one machine: not a
|
|
569
|
+
performance claim, but the ratios carry over roughly.
|
|
570
|
+
|
|
571
|
+
| Option | 48 × 48 | 128 × 128 | What it buys |
|
|
572
|
+
| --- | --- | --- | --- |
|
|
573
|
+
| default | 0.50 s (1×) | 2.49 s (1×) | Point optics, bilinear demosaic, sRGB samples |
|
|
574
|
+
| `lens.diffraction: "circular-aperture"` | 0.65 s (1.3×) | 2.84 s (1.1×) | Aperture-dependent sharpness (Airy blur); convolution 0.38 s at 128 |
|
|
575
|
+
| `output.demosaic: "ahd"` | 0.65 s (1.3×) | 2.60 s (1.0×) | About half the false colour on sharp edges; 5-site margin, demosaic 0.05 s at 128 |
|
|
576
|
+
| `sensor.antiAliasing: { kind: "four-spot" }` | 0.94 s (1.9×) | 4.96 s (2.0×) | Optical low-pass filter: false colour halved again, at a cost in sharpness |
|
|
577
|
+
| anti-aliasing + diffraction | 1.16 s (2.3×) | 6.02 s (2.4×) | Both, still on the fast convolution path |
|
|
578
|
+
| anti-aliasing + diffraction + AHD | 1.42 s (2.8×) | 6.99 s (2.8×) | The most faithful still image of a planar chart |
|
|
579
|
+
| `lens.pupilSampling` 1 × 4 rays | 1.79 s (3.6×) | 11.5 s (4.6×) | Defocus and depth of field, coarse |
|
|
580
|
+
| `lens.pupilSampling` 2 × 8 rays | 5.20 s (10×) | 33.1 s (13×) | Defocus and depth of field, smoother bokeh |
|
|
581
|
+
| `lens.psf` (caller 3 × 3 kernel) | 2.11 s (4.2×) | 12.4 s (5.0×) | Any sampled lens PSF; runs per tap, so cost grows with the kernel size (a 9 × 9 diffraction kernel took 13.1 s at 48 × 48) |
|
|
582
|
+
| `motion` yaw 0.01 rad/s (8 temporal samples) | 1.70 s (3.4×) | 8.74 s (3.5×) | Motion blur; cost grows with `sensor.temporalSampleCount` |
|
|
583
|
+
| rotation + `shutter: "rolling"` | 1.79 s (3.6×) | 9.24 s (3.7×) | Rolling-shutter skew of moving content |
|
|
584
|
+
| `motion.shutter: "rolling"` (still camera) | 0.47 s (0.9×) | 2.51 s (1.0×) | Per-row exposure windows; no cost alone |
|
|
585
|
+
| `output.format: "png"` | 0.50 s (1.0×) | 2.63 s (1.1×) | PNG file bytes; encode 16 ms at 128 |
|
|
586
|
+
| `output.format: "jpeg"` | 0.52 s (1.0×) | 2.59 s (1.0×) | Baseline JPEG; encode 45 ms at 128 |
|
|
587
|
+
| `output.format: "dng"` | 0.50 s (1.0×) | 2.27 s (0.9×) | Lossless CFA DNG of the RAW; encode 4 ms at 128 |
|
|
588
|
+
| `output.highlightMode: "reconstruct"` | 0.50 s (1.0×) | 2.20 s (0.9×) | Near-clipped highlights blend smoothly toward neutral instead of clipping per channel; no measurable cost |
|
|
589
|
+
| `output.whiteBalance: "auto"` | 0.77 s (1.6×) | 4.76 s (1.9×) | Grey-world white balance; renders the RAW a second time (2.24 s at 128) |
|
|
590
|
+
|
|
591
|
+
Ratios below about 1.1× are within the run-to-run spread (the three default runs at 128 × 128 took
|
|
592
|
+
2.23–2.50 s). The radiance work dominates: options that multiply the scene queries per site
|
|
593
|
+
(anti-aliasing spots, pupil rays, PSF taps, temporal samples) multiply the run time by about the same
|
|
594
|
+
factor, and output-side options cost almost nothing.
|
|
595
|
+
|
|
596
|
+
### Measuring cost on the user's device
|
|
597
|
+
|
|
598
|
+
From 2.1 every result says what ran and how long each stage took, so an application can decide on the
|
|
599
|
+
user's own device which options to offer:
|
|
600
|
+
|
|
601
|
+
- `diagnostics.options` lists the opt-ins that changed the work of the run, in the fixed order of
|
|
602
|
+
`SIMULATE_CAPTURE_COST_OPTIONS` (for example `["lens.diffraction", "sensor.antiAliasing",
|
|
603
|
+
"output.demosaic.ahd", "output.format.png"]`). It is empty when every default is used.
|
|
604
|
+
- `diagnostics.timingsMs` has `raw`, `whiteBalance` (the second RAW pass of `whiteBalance: "auto"`),
|
|
605
|
+
`development` (includes DNG writing), `jpeg` and `total` in whole milliseconds, and, where the stage
|
|
606
|
+
ran, `diffraction` (the object-space convolution, part of `raw`), `demosaic` (the AHD demosaic, part
|
|
607
|
+
of `development`) and `encode` (PNG, JPEG or DNG serialization), with sub-millisecond resolution where
|
|
608
|
+
the host has `performance.now()`.
|
|
609
|
+
|
|
610
|
+
Timings change from run to run, but nothing else does: they never enter the scene digest, the RAW,
|
|
611
|
+
the samples or the encoded files, and a test checks that two runs differ only in `timingsMs`.
|
|
612
|
+
|
|
613
|
+
```ts
|
|
614
|
+
import { simulateCapture } from "@photivra/engine";
|
|
615
|
+
|
|
616
|
+
const probe = await simulateCapture({ scene: { preset: "slanted-edge" }, output: { width: 32, height: 32, demosaic: "ahd" } });
|
|
617
|
+
console.log(probe.diagnostics.options, probe.diagnostics.timingsMs.total, probe.diagnostics.timingsMs.demosaic);
|
|
618
|
+
```
|
|
619
|
+
|
|
457
620
|
## Not available
|
|
458
621
|
|
|
459
622
|
The facade offers no atmospheric haze or polarizer (see [Optics](#optics)), flash, video, HDR brackets
|
|
@@ -462,6 +625,11 @@ provided, for rights reasons.
|
|
|
462
625
|
|
|
463
626
|
## Sources
|
|
464
627
|
|
|
628
|
+
- R. Palum, "Anti-Aliasing Filter Analysis for Digital Cameras", Proc. IS&T International Congress of
|
|
629
|
+
Imaging Science (ICIS '06), 2006, free at
|
|
630
|
+
<https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2006/ICIS-0-736/33683.pdf>
|
|
631
|
+
(birefringent quartz four-spot filter; one pixel wide in each direction cuts off at Nyquist; its MTF
|
|
632
|
+
is the modulus of a cosine that returns to one at the sampling frequency).
|
|
465
633
|
- CIPA DC-004-Translation-2020, *Sensitivity of Digital Cameras*, free at
|
|
466
634
|
<https://www.cipa.jp/e/std/std-sec.html> (standard output sensitivity S = 10/H, §3.2; reference
|
|
467
635
|
level tied to 18 % grey, §5.2 and Explanation 1, 2.2; focal-plane exposure H = 0.65·L·t/N², §5.5
|
package/docs/api/core-version.md
CHANGED
|
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
|
|
|
11
11
|
own versions. Release checks confirm it matches package.json and the packed package.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
ENGINE_API_VERSION = "2.
|
|
14
|
+
ENGINE_API_VERSION = "2.1.0" as const
|
|
15
15
|
```
|
package/docs/api/exports.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "2.
|
|
3
|
-
"engineApiVersion": "2.
|
|
2
|
+
"packageVersion": "2.1.0",
|
|
3
|
+
"engineApiVersion": "2.1.0",
|
|
4
4
|
"exports": [
|
|
5
5
|
{
|
|
6
6
|
"name": "AbsoluteUncertaintyEstimate",
|
|
@@ -2122,6 +2122,16 @@
|
|
|
2122
2122
|
"source": "src/color/white-balance.ts",
|
|
2123
2123
|
"kind": "type"
|
|
2124
2124
|
},
|
|
2125
|
+
{
|
|
2126
|
+
"name": "createDiffractionLimitedPsfProfile",
|
|
2127
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2128
|
+
"kind": "runtime"
|
|
2129
|
+
},
|
|
2130
|
+
{
|
|
2131
|
+
"name": "CreateDiffractionLimitedPsfProfileInput",
|
|
2132
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2133
|
+
"kind": "type"
|
|
2134
|
+
},
|
|
2125
2135
|
{
|
|
2126
2136
|
"name": "createDurandDorseyToneMappingTask",
|
|
2127
2137
|
"source": "src/processing/tone-mapping.ts",
|
|
@@ -2437,6 +2447,26 @@
|
|
|
2437
2447
|
"source": "src/optics/depth-of-field.ts",
|
|
2438
2448
|
"kind": "type"
|
|
2439
2449
|
},
|
|
2450
|
+
{
|
|
2451
|
+
"name": "DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS",
|
|
2452
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2453
|
+
"kind": "runtime"
|
|
2454
|
+
},
|
|
2455
|
+
{
|
|
2456
|
+
"name": "DIFFRACTION_LIMITED_PSF_PROFILE_MODEL",
|
|
2457
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2458
|
+
"kind": "runtime"
|
|
2459
|
+
},
|
|
2460
|
+
{
|
|
2461
|
+
"name": "DiffractionLimitedPsfProfile",
|
|
2462
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2463
|
+
"kind": "type"
|
|
2464
|
+
},
|
|
2465
|
+
{
|
|
2466
|
+
"name": "DiffractionLimitedPsfWavelength",
|
|
2467
|
+
"source": "src/optics/diffraction-psf-profile.ts",
|
|
2468
|
+
"kind": "type"
|
|
2469
|
+
},
|
|
2440
2470
|
{
|
|
2441
2471
|
"name": "DigitalGeometricTransform",
|
|
2442
2472
|
"source": "src/output/geometric-transforms.ts",
|
|
@@ -7692,6 +7722,11 @@
|
|
|
7692
7722
|
"source": "src/sensor/signal-noise.ts",
|
|
7693
7723
|
"kind": "type"
|
|
7694
7724
|
},
|
|
7725
|
+
{
|
|
7726
|
+
"name": "SIMULATE_CAPTURE_COST_OPTIONS",
|
|
7727
|
+
"source": "src/facade/simulate-capture.ts",
|
|
7728
|
+
"kind": "runtime"
|
|
7729
|
+
},
|
|
7695
7730
|
{
|
|
7696
7731
|
"name": "SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX",
|
|
7697
7732
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -7722,6 +7757,11 @@
|
|
|
7722
7757
|
"source": "src/facade/simulate-capture.ts",
|
|
7723
7758
|
"kind": "type"
|
|
7724
7759
|
},
|
|
7760
|
+
{
|
|
7761
|
+
"name": "SimulateCaptureCostOption",
|
|
7762
|
+
"source": "src/facade/simulate-capture.ts",
|
|
7763
|
+
"kind": "type"
|
|
7764
|
+
},
|
|
7725
7765
|
{
|
|
7726
7766
|
"name": "SimulateCaptureDemosaic",
|
|
7727
7767
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -7762,6 +7802,11 @@
|
|
|
7762
7802
|
"source": "src/facade/simulate-capture.ts",
|
|
7763
7803
|
"kind": "type"
|
|
7764
7804
|
},
|
|
7805
|
+
{
|
|
7806
|
+
"name": "SimulateCaptureSensorAntiAliasing",
|
|
7807
|
+
"source": "src/facade/simulate-capture.ts",
|
|
7808
|
+
"kind": "type"
|
|
7809
|
+
},
|
|
7765
7810
|
{
|
|
7766
7811
|
"name": "SimulateCaptureWhiteBalance",
|
|
7767
7812
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
|
|
4
4
|
|
|
5
|
+
## SIMULATE_CAPTURE_COST_OPTIONS
|
|
6
|
+
|
|
7
|
+
Options reported in `diagnostics.options` (2.1), in this order, when they ran: the ones that change how much work
|
|
8
|
+
a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
12
|
+
"lens.diffraction", "sensor.antiAliasing", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror",
|
|
13
|
+
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
14
|
+
"output.format.jpeg", "output.format.png", "output.format.dng"] as const)
|
|
15
|
+
```
|
|
16
|
+
|
|
5
17
|
## SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX
|
|
6
18
|
|
|
7
19
|
Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
@@ -70,12 +82,22 @@ PQ and HLG (BT.2100) are not part of 2.0; see THIRD_PARTY.md.
|
|
|
70
82
|
export type SimulateCaptureColorEncoding = "srgb" | "display-p3" | "rec2020";
|
|
71
83
|
```
|
|
72
84
|
|
|
85
|
+
## SimulateCaptureCostOption
|
|
86
|
+
|
|
87
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
export type SimulateCaptureCostOption = typeof SIMULATE_CAPTURE_COST_OPTIONS[number];
|
|
91
|
+
```
|
|
92
|
+
|
|
73
93
|
## SimulateCaptureDemosaic
|
|
74
94
|
|
|
75
|
-
|
|
95
|
+
Demosaic. `bilinear`: the built-in Bayer kernels (`createBuiltInBayerDemosaicProfiles`). `ahd` (2.1): adaptive
|
|
96
|
+
homogeneity-directed demosaicing (`createAhdDemosaicTask`, after Hirakawa & Parks 2005), a non-linear
|
|
97
|
+
edge-directed method that reduces zipper and false colour on sharp edges.
|
|
76
98
|
|
|
77
99
|
```ts
|
|
78
|
-
export type SimulateCaptureDemosaic = "bilinear";
|
|
100
|
+
export type SimulateCaptureDemosaic = "bilinear" | "ahd";
|
|
79
101
|
```
|
|
80
102
|
|
|
81
103
|
## SimulateCaptureExposureMode
|
|
@@ -139,6 +161,13 @@ export interface SimulateCaptureOptions {
|
|
|
139
161
|
* focus must equal the resolved capture settings exactly. Not combinable with pupilSampling.
|
|
140
162
|
*/
|
|
141
163
|
psf?: Extract<CalculateEnvironmentSensorPhotoSignalInput["psf"], { kind: "sampled-local" }>;
|
|
164
|
+
/**
|
|
165
|
+
* Opt-in (2.1) diffraction blur of an ideal, aberration-free, in-focus lens with a circular pupil at the
|
|
166
|
+
* capture's resolved f-number: the engine builds the Airy kernels (`createDiffractionLimitedPsfProfile`)
|
|
167
|
+
* on the site pitch. `kernelRadiusSamples` (0–32) sets the kernel window; default: the third dark ring at
|
|
168
|
+
* the longest spectral node. Not combinable with `psf` or `pupilSampling`. Default off.
|
|
169
|
+
*/
|
|
170
|
+
diffraction?: SimulateCaptureLensDiffraction;
|
|
142
171
|
};
|
|
143
172
|
exposure?: {
|
|
144
173
|
/** Default "manual". */
|
|
@@ -201,6 +230,11 @@ export interface SimulateCaptureOptions {
|
|
|
201
230
|
temporalSampleCount?: number;
|
|
202
231
|
/** Noise seed (uint32). Default 1. */
|
|
203
232
|
noiseSeed?: number;
|
|
233
|
+
/**
|
|
234
|
+
* Opt-in (2.1) optical anti-aliasing filter (birefringent point splitting, `SimulateCaptureSensorAntiAliasing`).
|
|
235
|
+
* Multiplies the scene queries by the number of split points. Default off (the tiers declare no filter).
|
|
236
|
+
*/
|
|
237
|
+
antiAliasing?: SimulateCaptureSensorAntiAliasing;
|
|
204
238
|
};
|
|
205
239
|
/** Aborting cancels the running task; the promise rejects with a domain error and returns no partial image. */
|
|
206
240
|
signal?: AbortSignal;
|
|
@@ -262,6 +296,14 @@ export interface SimulateCaptureResult {
|
|
|
262
296
|
siteInput(nativeIndex: number): SimulateEnvironmentSensorRawFrameInput["sites"][number];
|
|
263
297
|
development: Omit<NativeRawDevelopmentInput, "raw">;
|
|
264
298
|
whiteBalanceState: ResolvedWhiteBalanceState | null;
|
|
299
|
+
/**
|
|
300
|
+
* Present with lens.diffraction (2.1): the engine-built Airy profile, the equivalent environment sampled PSF
|
|
301
|
+
* selection, and how it was applied. With `object-space-convolution` the site inputs declare no PSF and the
|
|
302
|
+
* provider answers with the PSF-convolved radiance; putting `psf` into each site input and using the plain
|
|
303
|
+
* scene provider gives the same expected signal (to float rounding) through the per-tap environment quadrature.
|
|
304
|
+
*/
|
|
305
|
+
diffraction?: { profile: DiffractionLimitedPsfProfile; psf: Extract<CalculateEnvironmentSensorPhotoSignalInput["psf"], { kind: "sampled-local" }>;
|
|
306
|
+
execution: "object-space-convolution" | "environment-psf-quadrature" };
|
|
265
307
|
};
|
|
266
308
|
exposure: {
|
|
267
309
|
mode: SimulateCaptureExposureMode;
|
|
@@ -295,7 +337,20 @@ export interface SimulateCaptureResult {
|
|
|
295
337
|
rendering: SdrRenderingResult["diagnostics"];
|
|
296
338
|
/** Present with output.gamutMapping "perceptual": pixels whose chroma or lightness the mapping changed. */
|
|
297
339
|
gamutMapping?: { method: "cielab-hue-preserving-chroma-compression"; kneeFraction: number; adjustedPixelCount: number; lightnessClippedPixelCount: number };
|
|
298
|
-
|
|
340
|
+
/**
|
|
341
|
+
* Wall-clock milliseconds per stage. `raw`, `whiteBalance` (the second RAW pass of auto white balance), `development`
|
|
342
|
+
* (includes DNG writing), `jpeg` and `total` are whole milliseconds (`Date.now()`). Since 2.1, where a stage ran:
|
|
343
|
+
* `diffraction` (the object-space convolution of `lens.diffraction`; part of `raw` and `whiteBalance`), `demosaic`
|
|
344
|
+
* (the AHD demosaic; part of `development`) and `encode` (PNG, JPEG or DNG serialization), with sub-millisecond
|
|
345
|
+
* resolution where the host has `performance.now()`. Timings vary from run to run and never enter any digest or output byte.
|
|
346
|
+
*/
|
|
347
|
+
timingsMs: { raw: number; whiteBalance: number; development: number; jpeg: number; total: number; diffraction?: number; demosaic?: number; encode?: number };
|
|
348
|
+
/**
|
|
349
|
+
* 2.1: the opt-in options that changed the work of this run, in a fixed order (see `SIMULATE_CAPTURE_COST_OPTIONS`).
|
|
350
|
+
* Empty for a capture with every default. With `timingsMs` it lets an application measure each option's cost on
|
|
351
|
+
* the user's device.
|
|
352
|
+
*/
|
|
353
|
+
options: readonly SimulateCaptureCostOption[];
|
|
299
354
|
};
|
|
300
355
|
provenance: {
|
|
301
356
|
scientificStatus: "approximation";
|
|
@@ -310,6 +365,24 @@ export interface SimulateCaptureResult {
|
|
|
310
365
|
}
|
|
311
366
|
```
|
|
312
367
|
|
|
368
|
+
## SimulateCaptureSensorAntiAliasing
|
|
369
|
+
|
|
370
|
+
Optical anti-aliasing (low-pass) filter (2.1), modelled as birefringent point splitting: each image point is
|
|
371
|
+
split into equally bright copies (the engine's `normalized-point-splitting-kernel` optical-stack response).
|
|
372
|
+
- `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)|.
|
|
373
|
+
- `two-spot`: one plate splitting along `axis`; copies at ±d/2, weight 1/2 each, MTF |cos(π·f·d)|.
|
|
374
|
+
- `point-splitting`: the engine contract itself, offsets in micrometres (+x right, +y down) and positive weights
|
|
375
|
+
summing to 1 (at most 16 components, each within 4 sites).
|
|
376
|
+
d = `separationSites` × pixel pitch, 0 < separationSites ≤ 4, default 1 (first MTF zero at the sensor's Nyquist
|
|
377
|
+
frequency 1/(2·pitch)).
|
|
378
|
+
|
|
379
|
+
```ts
|
|
380
|
+
export type SimulateCaptureSensorAntiAliasing =
|
|
381
|
+
| { kind: "four-spot"; separationSites?: number }
|
|
382
|
+
| { kind: "two-spot"; axis: "horizontal" | "vertical"; separationSites?: number }
|
|
383
|
+
| { kind: "point-splitting"; components: readonly AntiAliasingPointSplitComponent[] };
|
|
384
|
+
```
|
|
385
|
+
|
|
313
386
|
## SimulateCaptureWhiteBalance
|
|
314
387
|
|
|
315
388
|
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# optics/diffraction-psf-profile.ts public contracts
|
|
2
|
+
|
|
3
|
+
The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
|
|
4
|
+
|
|
5
|
+
## createDiffractionLimitedPsfProfile
|
|
6
|
+
|
|
7
|
+
Creates a sampled lens PSF profile for an ideal diffraction-limited lens with a circular pupil: one
|
|
8
|
+
unit-sum Airy kernel per wavelength on one shared (2R+1)² grid, with the truncated energy reported per
|
|
9
|
+
wavelength. The profile declares the given focal length, f-number and focus exactly, and the field axes
|
|
10
|
+
±`fieldHalfExtentMm` with the same kernel at every field node.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
export function createDiffractionLimitedPsfProfile(input: CreateDiffractionLimitedPsfProfileInput): CalculationResult<DiffractionLimitedPsfProfile>;
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
This call is synchronous; any supplied provider must follow its explicit synchronous contract.
|
|
17
|
+
|
|
18
|
+
## CreateDiffractionLimitedPsfProfileInput
|
|
19
|
+
|
|
20
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
export interface CreateDiffractionLimitedPsfProfileInput {
|
|
24
|
+
/** Only `circular` in 2.1.0 (polygon pupils: `calculateIdealPolygonDiffractionPsf` gives point densities). */
|
|
25
|
+
aperture: "circular";
|
|
26
|
+
focalLengthMm: number;
|
|
27
|
+
apertureFNumber: number;
|
|
28
|
+
focus: FocusPlane;
|
|
29
|
+
/** Strictly increasing wavelengths, nm, in the medium of propagation (no air/vacuum conversion is applied). 1–64. */
|
|
30
|
+
wavelengthsNm: readonly number[];
|
|
31
|
+
/** Square sample pitch of every kernel, µm. */
|
|
32
|
+
samplePitchMicrometers: number;
|
|
33
|
+
/** Kernel half-width in samples; each kernel is (2R + 1)². 0–32. */
|
|
34
|
+
kernelRadiusSamples: number;
|
|
35
|
+
/** Half-extent of the image field over which the on-axis PSF is declared (profile field axes ±x, ±y), mm. */
|
|
36
|
+
fieldHalfExtentMm: { x: number; y: number };
|
|
37
|
+
profileId: string;
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS
|
|
42
|
+
|
|
43
|
+
Input limits: at most 64 wavelengths, kernel radius 0–32 samples (the irradiance-convolution limit).
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
DIFFRACTION_LIMITED_PSF_PROFILE_LIMITS = Object.freeze({ maximumWavelengths: 64, maximumKernelRadiusSamples: 32 })
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## DIFFRACTION_LIMITED_PSF_PROFILE_MODEL
|
|
50
|
+
|
|
51
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
DIFFRACTION_LIMITED_PSF_PROFILE_MODEL = Object.freeze({
|
|
55
|
+
modelId: "photivra-diffraction-limited-circular-pupil-psf" as const, modelVersion: "1.0.0" as const })
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## DiffractionLimitedPsfProfile
|
|
59
|
+
|
|
60
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
export interface DiffractionLimitedPsfProfile {
|
|
64
|
+
profile: LensSampledPsfProfile;
|
|
65
|
+
model: typeof DIFFRACTION_LIMITED_PSF_PROFILE_MODEL;
|
|
66
|
+
pupilDiameterMm: number;
|
|
67
|
+
pupilToImageDistanceMm: number;
|
|
68
|
+
workingFNumber: number;
|
|
69
|
+
kernelRadiusSamples: number;
|
|
70
|
+
samplePitchMicrometers: number;
|
|
71
|
+
wavelengths: readonly DiffractionLimitedPsfWavelength[];
|
|
72
|
+
/** Largest truncated energy over the wavelengths. */
|
|
73
|
+
maximumTruncatedEnergyFraction: number;
|
|
74
|
+
quadrature: { method: "polar-frequency-domain-trapezoid-gauss-legendre"; maximumRadialNodes: number; maximumAngularNodes: number };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## DiffractionLimitedPsfWavelength
|
|
79
|
+
|
|
80
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
export interface DiffractionLimitedPsfWavelength {
|
|
84
|
+
wavelengthNm: number;
|
|
85
|
+
/** f_c = 1/(λ·N_w), cycles per µm. */
|
|
86
|
+
cutoffFrequencyCyclesPerMicrometer: number;
|
|
87
|
+
/** 1.21967·λ·N_w, µm. */
|
|
88
|
+
firstZeroRadiusMicrometers: number;
|
|
89
|
+
/** Σ of the cell energies inside the window before renormalization (of a total of 1). */
|
|
90
|
+
capturedEnergyFraction: number;
|
|
91
|
+
/** 1 − capturedEnergyFraction: the Airy tail energy outside the window, put back by renormalization. */
|
|
92
|
+
truncatedEnergyFraction: number;
|
|
93
|
+
/** Fraction of the renormalized kernel in the centre sample. */
|
|
94
|
+
centreSampleFraction: number;
|
|
95
|
+
}
|
|
96
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photivra/engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Scientific TypeScript engine for camera optics, exposure, focus, motion, sensor sampling, diffraction, and digital imaging calculations.",
|
|
6
6
|
"type": "module",
|
|
@@ -35,7 +35,8 @@
|
|
|
35
35
|
"dist/schema",
|
|
36
36
|
"dist/sensor",
|
|
37
37
|
"dist/simulation",
|
|
38
|
-
"dist/stabilization",
|
|
38
|
+
"dist/stabilization",
|
|
39
|
+
"dist/processing",
|
|
39
40
|
"!dist/capture/browser-native-reference-*",
|
|
40
41
|
"!dist/schema/camera.js",
|
|
41
42
|
"!dist/schema/camera.js.map",
|