@photivra/engine 2.17.0 → 2.19.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 +116 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/camera/sensor-equivalence.d.ts +156 -0
- package/dist/camera/sensor-equivalence.js +208 -0
- package/dist/camera/sensor-equivalence.js.map +1 -0
- package/dist/capture/focus-control.js +2 -75
- package/dist/capture/focus-control.js.map +1 -1
- package/dist/capture/native-environment-lean.d.ts +1 -0
- package/dist/capture/native-environment-lean.js +1 -1
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.d.ts +1 -0
- package/dist/capture/native-environment-optical-stage.js +2 -5
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-environment-raw.js +1 -5
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +5 -0
- package/dist/capture/native-environment-tabulated.js +1 -1
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/release-sequence.js +2 -47
- package/dist/capture/release-sequence.js.map +1 -1
- package/dist/color/white-balance.d.ts +2 -0
- package/dist/color/white-balance.js +3 -56
- package/dist/color/white-balance.js.map +1 -1
- package/dist/composition/image-formation-plan.d.ts +1 -0
- package/dist/composition/image-formation-plan.js +3 -49
- package/dist/composition/image-formation-plan.js.map +1 -1
- package/dist/core/evidence-provenance.js +1 -16
- package/dist/core/evidence-provenance.js.map +1 -1
- package/dist/core/record-validation.d.ts +54 -0
- package/dist/core/record-validation.js +176 -0
- package/dist/core/record-validation.js.map +1 -1
- package/dist/core/scientific-assurance.js +1 -18
- package/dist/core/scientific-assurance.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +33 -0
- package/dist/core/shared-helpers.js +34 -0
- package/dist/core/shared-helpers.js.map +1 -0
- package/dist/core/spectral-composition.js +1 -36
- package/dist/core/spectral-composition.js.map +1 -1
- package/dist/core/spectral.js +1 -19
- package/dist/core/spectral.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/exposure-capabilities.d.ts +1 -0
- package/dist/equipment/exposure-capabilities.js +2 -36
- package/dist/equipment/exposure-capabilities.js.map +1 -1
- package/dist/equipment/ideal-lens.d.ts +25 -0
- package/dist/equipment/ideal-lens.js +21 -0
- package/dist/equipment/ideal-lens.js.map +1 -0
- package/dist/equipment/iso-capabilities.js +2 -49
- package/dist/equipment/iso-capabilities.js.map +1 -1
- package/dist/equipment/metering-capabilities.js +1 -26
- package/dist/equipment/metering-capabilities.js.map +1 -1
- package/dist/equipment/release-capabilities.js +1 -53
- package/dist/equipment/release-capabilities.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.js +1 -7
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
- package/dist/exposure/exposure-duration-control.js +1 -16
- package/dist/exposure/exposure-duration-control.js.map +1 -1
- package/dist/exposure/exposure-mode-resolver.js +1 -17
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/filtered-exposure-time.d.ts +93 -0
- package/dist/exposure/filtered-exposure-time.js +129 -0
- package/dist/exposure/filtered-exposure-time.js.map +1 -0
- package/dist/exposure/flash-ttl.d.ts +1 -0
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -49
- package/dist/exposure/flash.js.map +1 -1
- package/dist/exposure/metering-scene-radiance.js +1 -25
- package/dist/exposure/metering-scene-radiance.js.map +1 -1
- package/dist/exposure/metering-target.js +1 -30
- package/dist/exposure/metering-target.js.map +1 -1
- package/dist/exposure/metering-temporal.js +1 -8
- package/dist/exposure/metering-temporal.js.map +1 -1
- package/dist/exposure/metering.js +1 -72
- package/dist/exposure/metering.js.map +1 -1
- package/dist/exposure/program-line.js +1 -45
- package/dist/exposure/program-line.js.map +1 -1
- package/dist/facade/catalogue-lens-signal.d.ts +94 -0
- package/dist/facade/catalogue-lens-signal.js +362 -0
- package/dist/facade/catalogue-lens-signal.js.map +1 -0
- package/dist/facade/defocus-image-space-provider.js +5 -2
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/develop-joined-parts.d.ts +29 -0
- package/dist/facade/develop-joined-parts.js +33 -0
- package/dist/facade/develop-joined-parts.js.map +1 -0
- package/dist/facade/display-view-lens-signal.d.ts +32 -1
- package/dist/facade/display-view-lens-signal.js +85 -10
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +8 -0
- package/dist/facade/expected-signal-setup.js +7 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +5 -0
- package/dist/facade/expected-signal.js +5 -1
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +5 -2
- package/dist/facade/generic-lens-capture.js +5 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +67 -8
- package/dist/facade/simulate-capture.js +262 -126
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +1 -1
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +1 -3
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +1 -5
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -1
- package/dist/gpu/display-view-quality-gpu.js +4 -8
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.js +3 -25
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.js +1 -5
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +2 -0
- package/dist/gpu/webgpu-runtime.js +3 -0
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/motion/camera-rotation.js +1 -6
- package/dist/motion/camera-rotation.js.map +1 -1
- package/dist/motion/capture-rotation-inverse-mapping.d.ts +12 -0
- package/dist/motion/capture-rotation-inverse-mapping.js +2 -2
- package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-rotation-temporal-quadrature.js +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
- package/dist/motion/capture-rotation-trajectory.js +2 -17
- package/dist/motion/capture-rotation-trajectory.js.map +1 -1
- package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js +3 -14
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
- package/dist/motion/extended-object-projection.js +5 -40
- package/dist/motion/extended-object-projection.js.map +1 -1
- package/dist/optics/atmospheric-haze.js +1 -7
- package/dist/optics/atmospheric-haze.js.map +1 -1
- package/dist/optics/brown-conrady-distortion.js +1 -7
- package/dist/optics/brown-conrady-distortion.js.map +1 -1
- package/dist/optics/complex-pupil-psf.d.ts +1 -0
- package/dist/optics/complex-pupil-psf.js +2 -84
- package/dist/optics/complex-pupil-psf.js.map +1 -1
- package/dist/optics/focus-state.js +1 -11
- package/dist/optics/focus-state.js.map +1 -1
- package/dist/optics/front-of-lens-filter.d.ts +5 -0
- package/dist/optics/front-of-lens-filter.js +3 -65
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/generic-nd-filter-presets.d.ts +12 -0
- package/dist/optics/generic-nd-filter-presets.js +48 -0
- package/dist/optics/generic-nd-filter-presets.js.map +1 -0
- package/dist/optics/illumination-vignetting.js +1 -6
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +4 -1
- package/dist/optics/lens-psf-profile.js +12 -111
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/longitudinal-chromatic-aberration.js +1 -7
- package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -1
- package/dist/optics/pupil-vignetting.js +1 -7
- package/dist/optics/pupil-vignetting.js.map +1 -1
- package/dist/optics/radial-distortion.js +1 -6
- package/dist/optics/radial-distortion.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.js +4 -96
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/stray-light.d.ts +1 -0
- package/dist/optics/stray-light.js +1 -1
- package/dist/optics/stray-light.js.map +1 -1
- package/dist/optics/zernike-wavefront.js +1 -7
- package/dist/optics/zernike-wavefront.js.map +1 -1
- package/dist/output/capture-geometry.js +1 -8
- package/dist/output/capture-geometry.js.map +1 -1
- package/dist/output/developed-lens-corrections.d.ts +62 -0
- package/dist/output/developed-lens-corrections.js +135 -0
- package/dist/output/developed-lens-corrections.js.map +1 -0
- package/dist/output/generic-lens-corrections.d.ts +2 -6
- package/dist/output/generic-lens-corrections.js +3 -6
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/geometric-transforms.js +1 -4
- package/dist/output/geometric-transforms.js.map +1 -1
- package/dist/output/native-capture-sdr.d.ts +2 -2
- package/dist/output/native-capture-sdr.js +3 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/processing/development-detail.d.ts +29 -0
- package/dist/processing/development-detail.js +128 -107
- package/dist/processing/development-detail.js.map +1 -1
- package/dist/schema/illumination-temporal.js +3 -72
- package/dist/schema/illumination-temporal.js.map +1 -1
- package/dist/schema/illumination.d.ts +1 -0
- package/dist/schema/illumination.js +2 -65
- package/dist/schema/illumination.js.map +1 -1
- package/dist/schema/scene-radiance-temporal.js +2 -14
- package/dist/schema/scene-radiance-temporal.js.map +1 -1
- package/dist/schema/scene-radiance.js +2 -60
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/schema/validation.d.ts +2 -0
- package/dist/schema/validation.js +2 -17
- package/dist/schema/validation.js.map +1 -1
- package/dist/sensor/accumulated-charge.js +2 -59
- package/dist/sensor/accumulated-charge.js.map +1 -1
- package/dist/sensor/architecture.js +1 -16
- package/dist/sensor/architecture.js.map +1 -1
- package/dist/sensor/camera-saturation-capacity.d.ts +1 -0
- package/dist/sensor/camera-saturation-capacity.js +3 -55
- package/dist/sensor/camera-saturation-capacity.js.map +1 -1
- package/dist/sensor/capture-color-sampling-binding.js +1 -26
- package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
- package/dist/sensor/capture-mode-timing.js +1 -39
- package/dist/sensor/capture-mode-timing.js.map +1 -1
- package/dist/sensor/capture-mode.js +1 -23
- package/dist/sensor/capture-mode.js.map +1 -1
- package/dist/sensor/color-sampling.js +1 -26
- package/dist/sensor/color-sampling.js.map +1 -1
- package/dist/sensor/constant-rate-temporal-integration.js +2 -46
- package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
- package/dist/sensor/dark-current.js +2 -45
- package/dist/sensor/dark-current.js.map +1 -1
- package/dist/sensor/eqe-electron-rate.js +2 -35
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +5 -0
- package/dist/sensor/exposure-window.js +7 -22
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/iso-gain.js +1 -7
- package/dist/sensor/iso-gain.js.map +1 -1
- package/dist/sensor/iso-signal-chain.js +1 -43
- package/dist/sensor/iso-signal-chain.js.map +1 -1
- package/dist/sensor/optical-stack.js +1 -25
- package/dist/sensor/optical-stack.js.map +1 -1
- package/dist/sensor/photon-energy.js +1 -26
- package/dist/sensor/photon-energy.js.map +1 -1
- package/dist/sensor/physical-charge-capacity.js +3 -77
- package/dist/sensor/physical-charge-capacity.js.map +1 -1
- package/dist/sensor/radiometry-readiness.js +2 -23
- package/dist/sensor/radiometry-readiness.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +3 -0
- package/dist/sensor/raw-readout.js +4 -66
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/raw-reconstruction.js +2 -64
- package/dist/sensor/raw-reconstruction.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -9
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/dist/sensor/readout-timing.js +3 -57
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.d.ts +2 -0
- package/dist/sensor/response-application-compatibility.js +3 -29
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/response-operating-range.d.ts +1 -0
- package/dist/sensor/response-operating-range.js +3 -79
- package/dist/sensor/response-operating-range.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +4 -108
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sampling-aperture.js +1 -25
- package/dist/sensor/sampling-aperture.js.map +1 -1
- package/dist/sensor/spatial-sample-reduction.js +2 -19
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spatial-sampling-quadrature.js +1 -3
- package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
- package/dist/sensor/spatio-spectral-reduction.js +1 -18
- package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
- package/dist/sensor/spectral-coverage.d.ts +10 -1
- package/dist/sensor/spectral-coverage.js +1 -1
- package/dist/sensor/spectral-coverage.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +2 -35
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/spectral-response.js +1 -47
- package/dist/sensor/spectral-response.js.map +1 -1
- package/dist/stabilization/camera-shake.js +1 -6
- package/dist/stabilization/camera-shake.js.map +1 -1
- package/dist/stabilization/system.js +2 -80
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +69 -2
- package/docs/QUICK_START.md +136 -6
- package/docs/USAGE.md +89 -3
- package/docs/api/camera-sensor-equivalence.md +209 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-ideal-lens.md +26 -0
- package/docs/api/exports.json +118 -3
- package/docs/api/exposure-filtered-exposure-time.md +105 -0
- package/docs/api/facade-develop-joined-parts.md +32 -0
- package/docs/api/facade-simulate-capture.md +62 -8
- package/docs/api/optics-generic-nd-filter-presets.md +34 -0
- package/docs/api/optics-lens-psf-profile.md +3 -1
- package/docs/api/output-developed-lens-corrections.md +12 -0
- package/docs/api/output-generic-lens-corrections.md +0 -9
- package/llms.txt +3 -0
- package/package.json +1 -1
package/docs/QUICK_START.md
CHANGED
|
@@ -104,6 +104,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
104
104
|
| `output.exposureCompensationStops` | 0 | #556: a development exposure push, −2 to +5 stops, on the RAW before white balance (see [Development exposure push](#development-exposure-push-556)). |
|
|
105
105
|
| `output.diffractionCorrection` | off | #552: `{ wavelengthNm?, regularization?, maximumGain?, kernelRadiusPixels? }`, a bounded deconvolution of the diffraction blur; needs `lens.diffraction`. It never restores detail beyond the diffraction cut-off (see [Sharpening and diffraction correction](#sharpening-and-diffraction-correction-552)). |
|
|
106
106
|
| `output.sharpening` | off | #552: `{ radiusPixels?, amount?, threshold? }`, an unsharp mask after the tone curve; sRGB output only (see [Sharpening and diffraction correction](#sharpening-and-diffraction-correction-552)). |
|
|
107
|
+
| `output.lensCorrections` | off | #570, with `lens.catalogue`: `{ distortion?, lateralColor?, vignetting? }`, the lens's own corrections on the developed JPEG, PNG and samples; the DNG is never corrected. Works with `output.region` (see [Generic lens catalogue](#generic-lens-catalogue-549)). |
|
|
107
108
|
| `output.whiteBalance` | `"scene-illuminant"` | Also `"auto"`, `"none"`, `{ kelvin, tint? }` and `{ channelGains }`. |
|
|
108
109
|
| `output.jpegQuantizationStep` | 2 | JPEG only: constant step 1–255, not a manufacturer quality scale. |
|
|
109
110
|
| `sensor.pixelPitchMicrometers` | 6 | 0.5–1000 (2.5; 0.5–100 before). The sensor is `width × pitch` wide (see [Geometry](#geometry)); the ISO readout scales with the pixel area (see [ISO and pixel size](#iso-and-pixel-size)). Above 100 µm the capture is a downsized view of a whole frame (see [Whole-frame views](#whole-frame-views-25)). |
|
|
@@ -116,7 +117,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
116
117
|
| `motion.angularVelocityRadPerSec` | still | `{ pitch?, yaw?, roll? }` in rad/s (see [Motion](#motion)). |
|
|
117
118
|
| `motion.shutter` | `"global"` | Or `"rolling"`. |
|
|
118
119
|
| `motion.rollingShutterSkewSeconds` | the body's readout skew | Rolling only. |
|
|
119
|
-
| `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. |
|
|
120
|
+
| `signal`, `onProgress` | none | An `AbortSignal` cancels the running task; `onProgress` reports `raw`, `white-balance`, `development`, `diffraction-correction` and `sharpening` (#571: output rows, with those options) 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. |
|
|
120
121
|
|
|
121
122
|
Unknown fields are rejected, so a typo never silently falls back to a default.
|
|
122
123
|
|
|
@@ -212,10 +213,22 @@ and every number is a labelled approximation.
|
|
|
212
213
|
- **Combinations:** `lens.defocus` convolves the disc into the lens's PSF (a combined kernel up to 16 sites in radius);
|
|
213
214
|
`lens.focusScatter: { seed, shotIndex? }` moves the focus by a seeded autofocus error (`simulateGenericLensFocusError`), off
|
|
214
215
|
by default. `prime`, `focalLengthMm`, `projection`, `distortion`, `vignetting`, `psf`, `diffraction` and `pupilSampling` are
|
|
215
|
-
set by the lens and refused with it
|
|
216
|
+
set by the lens and refused with it. The display view renders `lens.catalogue` since #569, with the PSF taken per field zone
|
|
217
|
+
([Display view, Catalogue lenses](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#catalogue-lenses-569)).
|
|
216
218
|
- **Cost:** one scene query per kernel tap through the lens field mapping, like `lens.distortion`: a 48 × 48 region of a full frame
|
|
217
219
|
at 6 µm takes about 1 s with *PV 50 1.2 Pro* and 6 s with *PV 50 1.8* (Apple M5 Pro), against 0.01 s with `lens.prime`. Render
|
|
218
220
|
regions, or use a coarser `sensor.spectralStepNanometers`.
|
|
221
|
+
- **Lens corrections (#570):** `output: { lensCorrections: { distortion: true, lateralColor: true, vignetting: true } }` applies the
|
|
222
|
+
lens's generic correction profile (`createGenericLensCorrectionProfile`) to the developed linear colour, each correction
|
|
223
|
+
switched on separately, as cameras do for their JPEGs. The RAW and the DNG are never corrected. The image keeps its size and is
|
|
224
|
+
magnified just enough to have no missing pixels (`magnification` in the `output.lensCorrections` assumption, 1 for the barrel
|
|
225
|
+
wide angles); vignetting is corrected to a flat field. Regions and parts are byte-identical to the whole frame. The costs are
|
|
226
|
+
real: stretched corners lose resolution and brightened corners are noisier
|
|
227
|
+
([DIGITAL_OPTICS_FOUNDATION.md](https://github.com/Photivra/photivra/blob/main/docs/DIGITAL_OPTICS_FOUNDATION.md#lens-corrections-in-the-development-570)).
|
|
228
|
+
- **Fast method (#568):** `fast: true` (or `lens: { catalogue: { kind: "pv-50-1.8", fast: true } }`) blends the PSF over a field
|
|
229
|
+
grid and tabulates each site's signal, a labeled approximation within 2.3e-4 of the frame's peak signal of the exact method
|
|
230
|
+
(measured): a 768 × 512 whole full frame renders within 2× of the same `lens.prime` capture. It needs a still camera, site-center
|
|
231
|
+
queries and a chart or material scene; elsewhere the exact method runs (`diagnostics.fast` says which ran).
|
|
219
232
|
|
|
220
233
|
```ts
|
|
221
234
|
import { resolveGenericLens, simulateCapture } from "@photivra/engine";
|
|
@@ -233,6 +246,60 @@ const capture = await simulateCapture({
|
|
|
233
246
|
console.log(capture.assumptions.find((a) => a.id === "lens.catalogue.character")?.value);
|
|
234
247
|
```
|
|
235
248
|
|
|
249
|
+
### Ideal lens at any focal length and f-number (#576)
|
|
250
|
+
|
|
251
|
+
`lens.ideal: { focalLengthMm }` is a plain ideal lens: an aberration-free thin lens named by its **physical** focal length, on
|
|
252
|
+
whatever sensor the frame is (output width × height × pixel pitch). Unlike `lens.prime` and `lens.catalogue` it is not scaled by
|
|
253
|
+
the crop factor, so a 50 mm lens is 50 mm on full frame and on APS-C, as when a full-frame lens is mounted on a smaller sensor
|
|
254
|
+
(narrower field of view, the same blur in millimetres).
|
|
255
|
+
|
|
256
|
+
- **Any f-number within the bounds:** `lens.fNumber` from f/0.5 to f/128, focal lengths from 1 to 4000 mm (`IDEAL_LENS_LIMITS`).
|
|
257
|
+
f/0.5 is the physical limit of a lens in air (the sine condition: N = 1/(2·sin θ′) ≥ 0.5); the others are engine bounds. Values
|
|
258
|
+
beyond them are refused by name. Shutter priority and program may choose any aperture in that range (in dim light, wider
|
|
259
|
+
than real lenses open).
|
|
260
|
+
- **An optional f-number range:** `lens.ideal: { focalLengthMm, widestFNumber, narrowestFNumber }` (either or both, within
|
|
261
|
+
`IDEAL_LENS_LIMITS`, the widest no larger than the narrowest; each defaults to its bound) narrows that range like a real
|
|
262
|
+
lens's: `lens.fNumber` must lie in it, and shutter priority and program choose apertures only in it. The assumption then
|
|
263
|
+
records the range (`{ lensId, focalLengthMm, widestFNumber, narrowestFNumber }`); without it nothing changes. A range without
|
|
264
|
+
f/8 takes the reference exposure at its aperture nearest f/8 (the same N²/t). Where that shutter would be shorter than the
|
|
265
|
+
body's shortest (a range narrower than about f/1, such as f/0.5–f/0.9), the anchor takes the shortest shutter instead, a lower
|
|
266
|
+
N²/t that the meter and the exposure modes measure from, so the exposure is the same (#585; refused in every mode before 2.19.0).
|
|
267
|
+
- **Program with a range (#583):** program follows the engine's program line (f/16 1/2000 s … f/8 1/125 s … f/4 1.024 s). A
|
|
268
|
+
range that holds f/4–f/16 keeps it unchanged. Otherwise the line's f/4 and f/16 move to the range (each clamped to it): the
|
|
269
|
+
same shape, with the aperture held at the range's end and the shutter taking up the stops (the same N²/t, so the same
|
|
270
|
+
exposure), still from 1/2000 s to 1.024 s. Beyond the line's ends the resolver holds them, as before (with `iso: "auto"` the
|
|
271
|
+
ISO rises in dim light; with a fixed ISO the capture is under- or overexposed and `exposure.standardExposureDeviationStops`
|
|
272
|
+
says by how much). For example f/8–f/32 gives f/16 1/2000 s … f/16 1/125 s … f/8 1/125 s … f/8 1.024 s. The
|
|
273
|
+
`exposure.programLine` assumption then records the limited line (profile id `"facade-generic-program-line-lens-range"`, with its `nodes`). The same
|
|
274
|
+
applies to the `lens.prime` and `lens.catalogue` ranges that do not hold f/4–f/16 (the 800 mm and 1200 mm-equivalent primes;
|
|
275
|
+
*PV 800 5.6 Pro*, *PV 1200 8 Pro*, *PV 200 5.6*, *PV 300 5.6*, *PV 400 5.6*, *PV 500 5.6*, *PV 600 6.3* and *PV 800 9*),
|
|
276
|
+
which program refused before 2.19.0. An approximation: a generic line, not a manufacturer's program.
|
|
277
|
+
- **The same blur models as `lens.prime`:** `lens.defocus` (the thin-lens blur circle of `calculateDefocusCircle`), `lens.diffraction`
|
|
278
|
+
(the Airy pattern) and `lens.vignetting` (a caller profile). Without them the ideal lens renders sharp and evenly lit. It focuses
|
|
279
|
+
at any distance beyond the focal length (no minimum focus distance; the subject must be at least twice the focal length away, as
|
|
280
|
+
for every lens).
|
|
281
|
+
- **Capture and display view:** `simulateCapture`, `simulateDisplayView` and `prepareDisplayView` take it. It replaces the tier
|
|
282
|
+
lens's focal length and f-number range (its transmission is kept) and is recorded in the `lens.ideal` assumption
|
|
283
|
+
(`{ lensId: "ideal-50mm", focalLengthMm: 50 }`). At the same focal length and f-number it renders the same pixels as the tier
|
|
284
|
+
lens (`lens.focalLengthMm`) and the generic prime.
|
|
285
|
+
- **Not combinable** with `lens.prime`, `lens.catalogue` or `lens.focalLengthMm`.
|
|
286
|
+
|
|
287
|
+
A 50 mm f/1.8 lens on APS-C (23.5 × 15.6 mm: 235 × 156 pixels at 100 µm):
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { simulateCapture } from "@photivra/engine";
|
|
291
|
+
|
|
292
|
+
const capture = await simulateCapture({
|
|
293
|
+
scene: { preset: "siemens-star" },
|
|
294
|
+
output: { width: 235, height: 156 },
|
|
295
|
+
sensor: { pixelPitchMicrometers: 100 },
|
|
296
|
+
subjectDistanceM: 3,
|
|
297
|
+
lens: { ideal: { focalLengthMm: 50 }, fNumber: 1.8, focusDistanceM: 2.5, defocus: { model: "geometric-disc" } },
|
|
298
|
+
exposure: { mode: "aperture-priority" }
|
|
299
|
+
});
|
|
300
|
+
console.log(capture.assumptions.find((a) => a.id === "lens.defocus")?.value); // diameterMicrometers 94.5 on any sensor
|
|
301
|
+
```
|
|
302
|
+
|
|
236
303
|
### Fitting a chart to the frame (2.3)
|
|
237
304
|
|
|
238
305
|
`calculateSubjectDistanceForFraming({ sensor, focalLengthMm | equivalentFocalLengthMm, chart, fit? })`
|
|
@@ -485,7 +552,7 @@ Exposure compensation applies to the meter target first, as with a numeric ISO.
|
|
|
485
552
|
| `manual` | Your aperture and shutter; the ISO makes up the exposure (`resolveManualExposureMode`, automatic ISO). |
|
|
486
553
|
| `aperture-priority` | Your aperture. The shutter follows the meter at the lowest ISO, but no slower than `exposure.autoIso.slowestPreferredShutterSeconds` (required; seconds within the body's shutter range, or `"auto"`, see below); the ISO makes up the rest. At the Auto ISO maximum, `afterMaximumIso` decides: `"allow-slower-shutter"` (the default) lengthens the shutter to reach the target, `"hold-preferred-shutter"` keeps it and leaves the exposure under target (`resolveAperturePriorityAutoIsoExposureMode`). |
|
|
487
554
|
| `shutter-priority` | Your shutter. The aperture is chosen at the lowest ISO, no wider than the target needs, so it stops at the widest aperture; the ISO makes up the rest (`resolveShutterPriorityExposureMode`, policy `minimum-iso-aperture-first`). |
|
|
488
|
-
| `program` | The generic program line at the lowest ISO; beyond the line's end (f/4, 1.024 s) the ISO rises (`resolveProgramAutoExposureMode`, automatic ISO). With `exposure.autoIso.slowestPreferredShutterSeconds` (optional, a number or `"auto"`, #504) the line's shutter is held at it before the ISO rises, and `afterMaximumIso` applies as in aperture priority, never past the line's own shutter. |
|
|
555
|
+
| `program` | The generic program line (limited to the lens's f-number range, #583) at the lowest ISO; beyond the line's end (f/4, 1.024 s) the ISO rises (`resolveProgramAutoExposureMode`, automatic ISO). With `exposure.autoIso.slowestPreferredShutterSeconds` (optional, a number or `"auto"`, #504) the line's shutter is held at it before the ISO rises, and `afterMaximumIso` applies as in aperture priority, never past the line's own shutter. |
|
|
489
556
|
|
|
490
557
|
The result says what happened. `exposure.settings.iso` is the ISO used and `exposure.isoControl` is `"automatic"`.
|
|
491
558
|
`exposure.autoIso` reports `idealIsoBeforeConstraints`, the Auto ISO range (`autoIsoMinimum`, `autoIsoMaximum`),
|
|
@@ -923,10 +990,14 @@ console.log(result.diagnostics.detail); // { diffractionCorrection: {...}, sharp
|
|
|
923
990
|
noise; it adds no detail. Not with a wide-gamut `output.colorEncoding` or `output.gamutMapping: "perceptual"`.
|
|
924
991
|
- **Reported:** the `output.diffractionCorrection` and `output.sharpening` assumptions (settings, cut-off, measured filter
|
|
925
992
|
response, defaults used), `diagnostics.detail` (ringing, clipping and halo counts) and `diagnostics.options`.
|
|
926
|
-
- **Not combinable** with `output.region`; not offered by `simulateDisplayView` or `simulateIdealReference`.
|
|
927
|
-
|
|
928
|
-
|
|
993
|
+
- **Not combinable** with `output.region`; not offered by `simulateDisplayView` or `simulateIdealReference`. A capture rendered
|
|
994
|
+
in region parts takes them through `simulateCaptureFromParts` (#571, [Developing a capture rendered in parts](#developing-a-capture-rendered-in-parts-571)).
|
|
995
|
+
The DNG holds the RAW codes and is unchanged; only `image.samples` (and the JPEG or PNG made from them) change.
|
|
996
|
+
- **Cost:** both stages need the whole developed image, as AHD does. At 256 × 256 on an Apple M5 Pro (Node.js 24.19.0) the
|
|
929
997
|
development took 8 ms by default, 15 ms with sharpening and 18–20 ms with diffraction correction (R = 3–5), one run each.
|
|
998
|
+
Since #571 they run row by row in place, with the same sums in the same order (the same bytes as 2.18.0): one developed
|
|
999
|
+
image in memory instead of up to six, and `onProgress` reports each output row as the `diffraction-correction` and
|
|
1000
|
+
`sharpening` stages. At 24 MP see [Developing a capture rendered in parts](#developing-a-capture-rendered-in-parts-571).
|
|
930
1001
|
|
|
931
1002
|
The trade-offs of these and the lens corrections (corner resolution after distortion correction, corner noise after
|
|
932
1003
|
vignetting correction, residual longitudinal CA, halo overshoot, nothing beyond the cut-off) are measured in
|
|
@@ -1817,6 +1888,65 @@ const dng = await serializeSimulatedCaptureDngFromParts(dngFrame, parts);
|
|
|
1817
1888
|
console.log(dng.bytes.length);
|
|
1818
1889
|
```
|
|
1819
1890
|
|
|
1891
|
+
### Developing a capture rendered in parts (#571)
|
|
1892
|
+
|
|
1893
|
+
`simulateCaptureFromParts(options, parts)` develops the whole frame of `options` from region renders of the same
|
|
1894
|
+
request. It joins the parts' RAW exactly as `serializeSimulatedCaptureDngFromParts` does (the same parts, the same checks),
|
|
1895
|
+
then develops the joined RAW as `simulateCapture(options)` would, including the steps that filter across the whole image and
|
|
1896
|
+
that a region refuses: `output.diffractionCorrection` and `output.sharpening`. The joined RAW is the whole render's RAW, so the
|
|
1897
|
+
result's `image.samples`, JPEG or PNG bytes, `raw` and `plan` are those of the whole-frame capture, bit for bit. Pass the
|
|
1898
|
+
whole request (no `output.region`); render the parts with the same request plus `output.region`, without the detail stages,
|
|
1899
|
+
`output.dng` and auto white balance (none changes the RAW codes; auto white balance is measured here over the joined frame).
|
|
1900
|
+
`output.lensCorrections` (#570) is honored as in the whole-frame capture, bit for bit; a part may keep or drop it (a corrected
|
|
1901
|
+
region only widens its `region.nativeRect`). The corrections run on the whole developed frame and hold a second developed
|
|
1902
|
+
image while they resample it.
|
|
1903
|
+
|
|
1904
|
+
`onProgress` reports `development`, then `diffraction-correction` and `sharpening` once per output row of the whole image
|
|
1905
|
+
(each stage ends with `completedTileCount === totalTileCount`), then `jpeg`; no `raw` stage, since no RAW is rendered. A host
|
|
1906
|
+
can show these as "Applying diffraction correction to the whole image" and "Applying sharpening to the whole image". The
|
|
1907
|
+
lens corrections run between the two and report no stage of their own.
|
|
1908
|
+
`signal` cancels the step. In the result, `diagnostics.providerEvaluationCount` is 0, `diagnostics.timingsMs` are this call's
|
|
1909
|
+
(`raw` is the join), `diagnostics.fast` and the `raw.*` assumptions of how the RAW was rendered are in the parts' results
|
|
1910
|
+
instead, and a `parts` assumption records the join.
|
|
1911
|
+
|
|
1912
|
+
```ts
|
|
1913
|
+
import { simulateCaptureFromParts, simulateCapture, type SimulateCaptureDngPart, type SimulateCaptureOptions } from "@photivra/engine";
|
|
1914
|
+
|
|
1915
|
+
const whole: SimulateCaptureOptions = { scene: { preset: "slanted-edge" }, lens: { fNumber: 16, diffraction: "circular-aperture" },
|
|
1916
|
+
output: { width: 96, height: 64, format: "jpeg", diffractionCorrection: {}, sharpening: {} } };
|
|
1917
|
+
// Two strips rendered without the whole-image stages (in an application, in separate Web Workers).
|
|
1918
|
+
const parts: SimulateCaptureDngPart[] = await Promise.all([0, 32].map(async (y) => {
|
|
1919
|
+
const part = await simulateCapture({ ...whole, output: { width: 96, height: 64, region: { x: 0, y, width: 96, height: 32 } } });
|
|
1920
|
+
return { raw: part.raw, region: part.region! };
|
|
1921
|
+
}));
|
|
1922
|
+
const developed = await simulateCaptureFromParts({ ...whole, onProgress: ({ stage, completedTileCount, totalTileCount }) => {
|
|
1923
|
+
if (stage === "sharpening") console.log("Applying sharpening to the whole image", completedTileCount / totalTileCount);
|
|
1924
|
+
} }, parts);
|
|
1925
|
+
console.log(developed.image.jpeg!.length); // the bytes of simulateCapture(whole).image.jpeg
|
|
1926
|
+
```
|
|
1927
|
+
|
|
1928
|
+
Tests compare the result with the whole-frame capture byte for byte: JPEG with both stages, 16-bit sRGB with a thresholded
|
|
1929
|
+
mask, AHD with auto white balance, a development push and the four-spot filter, Display P3 PNG with diffraction correction,
|
|
1930
|
+
DNG with both stages, no stage at all, and a catalogue lens with all three lens corrections and sharpening (parts rendered
|
|
1931
|
+
with and without the corrections); and the row-by-row stages against whole-image passes on odd sizes.
|
|
1932
|
+
|
|
1933
|
+
**Measured at 24 MP** (6000 × 4000 at 6 µm, slanted edge at f/8 with `lens.diffraction`, 8 parts; Apple M5 Pro, Node.js 24.19.0,
|
|
1934
|
+
one thread, a shared machine, two runs; `node scripts/benchmark-joined-development.mjs`):
|
|
1935
|
+
|
|
1936
|
+
| Step | Time | Peak memory (process) |
|
|
1937
|
+
| --- | --- | --- |
|
|
1938
|
+
| Join and develop, no detail stage | 1.9–2.2 s (join 0.13–0.15 s) | 532–551 MiB |
|
|
1939
|
+
| … with sharpening (σ = 1) | 3.3–3.7 s (sharpening 1.9–2.1 s) | 1,080 MiB |
|
|
1940
|
+
| … with diffraction correction (R = 3, the default) | 3.7–3.9 s (correction 2.0 s) | 1,079–1,080 MiB |
|
|
1941
|
+
| … with both | 5.1–5.4 s (development 1.2 s, correction 2.0–2.1 s, sharpening 1.7–1.8 s) | 1,079 MiB |
|
|
1942
|
+
| Whole `simulateCapture` with both, for comparison | 10.0–10.4 s (RAW 4.8–4.9 s) | 924 MiB |
|
|
1943
|
+
|
|
1944
|
+
The peak includes the parts' RAW read back (7 bytes per native site) and the joined RAW. The detail stages' share is the one
|
|
1945
|
+
developed image in 64-bit floats (24 bytes per pixel, 549 MiB at 24 MP), held as before by AHD and the wide-gamut paths. In
|
|
1946
|
+
2.18.0 the stages held up to six such images: the same whole capture with both stages peaked at 3,670 MB (resident set
|
|
1947
|
+
sampled every 50 ms). The extra pass therefore costs about 5 s on one worker after the parts, against about 10 s to render
|
|
1948
|
+
the whole frame again on one worker.
|
|
1949
|
+
|
|
1820
1950
|
### A Print JPEG written in strips
|
|
1821
1951
|
|
|
1822
1952
|
At 1:1 (output size equal to the crop), a Print JPEG's rows can be coded on separate workers.
|
package/docs/USAGE.md
CHANGED
|
@@ -374,6 +374,8 @@ The actual focal length stays the physical optical quantity used by projection a
|
|
|
374
374
|
- Focus distance is not part of conventional 35 mm-equivalent focal length. A later digital/output crop and the output resolution are also excluded.
|
|
375
375
|
- If a product wants to describe the final digital framing, it should label it as output framing. It should not silently replace the optical focal length or the capture-equivalent focal length.
|
|
376
376
|
|
|
377
|
+
To render a lens by its physical focal length on any sensor, give `simulateCapture()` or the display view `lens.ideal: { focalLengthMm }` with any `lens.fNumber` within `IDEAL_LENS_LIMITS` (f/0.5–f/128, 1–4000 mm; optional `widestFNumber` and `narrowestFNumber` narrow the f-number range for the automatic exposure modes; in program the engine's program line is limited to that range, #583): an aberration-free thin lens that is not scaled by the crop factor, unlike `lens.prime` (see docs/QUICK_START.md, "Ideal lens at any focal length and f-number").
|
|
378
|
+
|
|
377
379
|
`calculateImagingAreaMetrics()` provides the same canonical diagonal crop-factor calculation, independent of raster density, so other engine modules do not need to repeat that formula.
|
|
378
380
|
|
|
379
381
|
## Focus-breathing projection
|
|
@@ -1056,6 +1058,30 @@ T_stack = T_1 * T_2 * ... * T_n
|
|
|
1056
1058
|
|
|
1057
1059
|
The stack keeps each filter's identity and evidence. The combined numeric uncertainty is deliberately reported as **not propagated**, unless it is established whether the filters' errors are correlated or independent. Each filter's own uncertainty stays attached to that filter.
|
|
1058
1060
|
|
|
1061
|
+
### Exposure time with an ND filter
|
|
1062
|
+
|
|
1063
|
+
`calculateFilteredExposureTime({ shutterSeconds, filter, shutterDialSeconds? })` (model `filtered-exposure-time` 1.0.0, #578) gives the shutter time that keeps the same exposure at the same f-number and ISO after adding one filter or a stack (`filter` takes one profile or an array in light-path order): `filteredShutterSeconds = shutterSeconds / T`, with `T` the stack's combined transmittance from `composeFrontOfLensFilterTransmission()`. It also returns `attenuationStops`, `exposureFactor` (`1/T`) and the full `filterTransmission`. With a `shutterDialSeconds` list (for example `GENERIC_THIRD_STOP_SHUTTER_SECONDS`) it adds `dial`: the nearest dial value in stops (the shorter on a tie), its exposure error in stops, and `clamped: "maximum"` when the exact time is longer than the longest dial value (use bulb).
|
|
1064
|
+
|
|
1065
|
+
- Exact under the linear sensor model (exposure `H = E·t`); film reciprocity failure is not modelled.
|
|
1066
|
+
- Nominal filters vary: a "10-stop" filter (`T = 2^-10`) gives 1/125 s → 8.19 s, an ND1000 (OD 3.0, 9.97 stops) gives exactly 8 s. The result is only as good as the profile, and is an approximation whenever a profile is.
|
|
1067
|
+
- Generic presets: `GENERIC_ND_FILTER_PROFILES[3]`, `[6]` and `[10]` (keys in `GENERIC_ND_FILTER_STOPS`) are frozen, unbranded, spectrally flat profiles over 380–780 nm with a transmittance of exactly 2^-N (1/8, 1/64, 1/1024). They are idealized (`scientificStatus: "approximation"`, uncertainty not quantified), and any function that takes a front-of-lens filter profile accepts them. For other filters, build a profile with `parseFrontOfLensFilterProfile()`.
|
|
1068
|
+
- Filters are resolved at 550 nm unless `wavelengthNanometers` is given; neutral filters do not depend on it. `incidentPolarization` matters only with a polarizer in the stack.
|
|
1069
|
+
|
|
1070
|
+
```ts
|
|
1071
|
+
import {
|
|
1072
|
+
GENERIC_ND_FILTER_PROFILES,
|
|
1073
|
+
GENERIC_THIRD_STOP_SHUTTER_SECONDS,
|
|
1074
|
+
calculateFilteredExposureTime
|
|
1075
|
+
} from "@photivra/engine";
|
|
1076
|
+
|
|
1077
|
+
const { value } = calculateFilteredExposureTime({
|
|
1078
|
+
shutterSeconds: 1 / 125,
|
|
1079
|
+
filter: GENERIC_ND_FILTER_PROFILES[10],
|
|
1080
|
+
shutterDialSeconds: GENERIC_THIRD_STOP_SHUTTER_SECONDS
|
|
1081
|
+
});
|
|
1082
|
+
console.log(value.filteredShutterSeconds.toFixed(2), value.dial?.shutterSeconds); // 8.19 8
|
|
1083
|
+
```
|
|
1084
|
+
|
|
1059
1085
|
### Spectral boundary
|
|
1060
1086
|
|
|
1061
1087
|
Spectral filter data stays wavelength-resolved all the way through the [scene-radiance-to-sensor bridge](#scene-radiance-to-sensor-irradiance). Photivra does not turn a spectral filter into an undocumented RGB tint.
|
|
@@ -5525,6 +5551,20 @@ All modes:
|
|
|
5525
5551
|
|
|
5526
5552
|
Flash-aware behaviour, safety shift and the program behaviour of named cameras are separate work.
|
|
5527
5553
|
|
|
5554
|
+
### The capture facade's program line and the lens's f-number range (#583)
|
|
5555
|
+
|
|
5556
|
+
`simulateCapture()` and the display view in `exposure.mode: "program"` use an engine-authored generic line (f/16 1/2000 s … f/8
|
|
5557
|
+
1/125 s … f/4 1.024 s, assumption `exposure.programLine`). `resolveProgramAutoExposureMode()` refuses a line whose apertures leave
|
|
5558
|
+
the capability range, so with a lens range that does not hold f/4–f/16 (`lens.ideal` `widestFNumber`/`narrowestFNumber`,
|
|
5559
|
+
`lens.prime`, `lens.catalogue`) the facade limits the line to the range first: the line's f/4 and f/16 are clamped to the range, giving the same shape with
|
|
5560
|
+
the aperture held at the range's end and the shutter keeping each point's exposure (t/N², camera equation H = 0.65·L·t/N²),
|
|
5561
|
+
still from 1/2000 s to 1.024 s. Where both lines run, the limited line's aperture is the old one clamped in log2 to the range.
|
|
5562
|
+
Beyond the line's ends the resolver holds them as before. A range that holds f/4–f/16, or no range, keeps the line and its
|
|
5563
|
+
bytes. The limited line is recorded as its profile (id `"facade-generic-program-line-lens-range"`), with node stops measured from the
|
|
5564
|
+
capture's exposure anchor (f/8, 1/125 s; the same N²/t at the range's aperture nearest f/8; or, #585, that aperture at the
|
|
5565
|
+
body's shortest shutter when the same N²/t would need a shorter one). An approximation of a generic
|
|
5566
|
+
program, not a manufacturer's.
|
|
5567
|
+
|
|
5528
5568
|
## Program Auto and Full Auto exposure
|
|
5529
5569
|
|
|
5530
5570
|
Program Auto and Full Auto exposure both need a declared, versioned **program line**: a curve that says which aperture and shutter to use at each brightness level. The engine does not invent one "correct" aperture/shutter pair.
|
|
@@ -7478,9 +7518,11 @@ own designs, not real products). `resolveGenericLens` resolves one for a sensor,
|
|
|
7478
7518
|
(scaled by the crop factor), aperture range, close focus, projection and its character, Seidel aberrations, axial and lateral
|
|
7479
7519
|
colour, distortion and vignetting profiles, flare, breathing and focus scatter, each a labelled approximation.
|
|
7480
7520
|
`simulateGenericLensFocusError` draws a seeded autofocus error; `createGenericLensCorrectionProfile` builds the lens's generic
|
|
7481
|
-
correction profile for `resolveLensCorrectionPlan
|
|
7482
|
-
|
|
7483
|
-
[
|
|
7521
|
+
correction profile for `resolveLensCorrectionPlan`, which `simulateCapture` applies to its developed image with
|
|
7522
|
+
`output.lensCorrections` (#570; never to the RAW or the DNG). `simulateCapture` renders a catalogue lens with `lens.catalogue`
|
|
7523
|
+
([quick start](QUICK_START.md#generic-lens-catalogue-549)); `fast: true` (or `lens: { catalogue: { kind, fast: true } }`) takes its
|
|
7524
|
+
fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures. Equations, tables and the
|
|
7525
|
+
chart measurements: [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
|
|
7484
7526
|
|
|
7485
7527
|
```ts
|
|
7486
7528
|
import { GENERIC_LENS_CATALOGUE, resolveGenericLens, simulateGenericLensFocusError } from "@photivra/engine";
|
|
@@ -7739,6 +7781,12 @@ console.log(star.assumptions.filter((a) => a.kind === "default").map((a) => a.id
|
|
|
7739
7781
|
console.log(star.request.siteInput(0).environment.optics.nominalFNumber);
|
|
7740
7782
|
```
|
|
7741
7783
|
|
|
7784
|
+
A capture can render its RAW in parallel parts (`output.region`, one call per part). `serializeSimulatedCaptureDngFromParts()`
|
|
7785
|
+
writes the whole frame's DNG from the parts, and `simulateCaptureFromParts()` (#571) develops the whole frame from them,
|
|
7786
|
+
including the whole-image stages `output.diffractionCorrection` and `output.sharpening` that a region refuses, with the same
|
|
7787
|
+
bytes as a whole-frame `simulateCapture()` and per-row progress (`diffraction-correction`, `sharpening`). See
|
|
7788
|
+
[Quick start, Developing a capture rendered in parts](QUICK_START.md#developing-a-capture-rendered-in-parts-571).
|
|
7789
|
+
|
|
7742
7790
|
### Color charts (2.14)
|
|
7743
7791
|
|
|
7744
7792
|
The engine's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) are reflectance spectra lit by the capture's illuminant. Draw the
|
|
@@ -7999,3 +8047,41 @@ import { simulateCapture } from "@photivra/engine";
|
|
|
7999
8047
|
const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motion: { angularVelocityRadPerSec: { pitch: 0.02 }, shutter: "rolling" }, output: { width: 6, height: 6 } });
|
|
8000
8048
|
console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
|
|
8001
8049
|
```
|
|
8050
|
+
|
|
8051
|
+
## Equivalent f-number, sensor area and blur as a share of the frame
|
|
8052
|
+
|
|
8053
|
+
`calculateSensorEquivalence()` says what a lens on one sensor corresponds to on a 36 × 24 mm frame (or on a `reference` you pass). Call it once per sensor to fill a lens table:
|
|
8054
|
+
|
|
8055
|
+
```ts
|
|
8056
|
+
import { calculateBlurShareOfFrame, calculateSensorEquivalence } from "@photivra/engine";
|
|
8057
|
+
|
|
8058
|
+
const apsC = calculateSensorEquivalence({
|
|
8059
|
+
sensor: { widthMm: 23.5, heightMm: 15.6 },
|
|
8060
|
+
focalLengthMm: 50,
|
|
8061
|
+
fNumber: 2
|
|
8062
|
+
}).value;
|
|
8063
|
+
|
|
8064
|
+
console.log(apsC.cropFactor); // ≈ 1.53
|
|
8065
|
+
console.log(apsC.equivalentFocalLengthMm); // ≈ 76.7 (framing)
|
|
8066
|
+
console.log(apsC.equivalentFNumber); // ≈ 3.07 (depth of field, blur and total light; not exposure)
|
|
8067
|
+
console.log(apsC.exposureFNumber); // 2 (exposure does not change with sensor size)
|
|
8068
|
+
console.log(apsC.areaRatio); // ≈ 0.42 of the full-frame area
|
|
8069
|
+
console.log(apsC.totalLightDifferenceStops); // ≈ -1.24 stops at the same f-number and shutter
|
|
8070
|
+
console.log(apsC.basis.equivalentFNumber.kind); // "approximation"
|
|
8071
|
+
|
|
8072
|
+
const share = calculateBlurShareOfFrame({
|
|
8073
|
+
blurCircleMm: 0.03,
|
|
8074
|
+
sensor: { widthMm: 36, heightMm: 24 },
|
|
8075
|
+
print: { widthMm: 300, heightMm: 200 }
|
|
8076
|
+
}).value;
|
|
8077
|
+
|
|
8078
|
+
console.log(share.shareOfDiagonal); // ≈ 0.00069 of the frame diagonal
|
|
8079
|
+
console.log(share.print?.blurOnPrintMm); // 0.25 mm on the print
|
|
8080
|
+
```
|
|
8081
|
+
|
|
8082
|
+
- `basis` gives, for every number, `kind: "exact"` or `"approximation"` and the conditions. Crop factor, equivalent focal length, area ratio, total-light difference, entrance pupil and angles of view are exact under their stated conditions.
|
|
8083
|
+
- The equivalent f-number (crop factor × f-number) assumes the same framing, subject distance and output size. It is a thin-lens approximation that is exact only when the focus distance is much greater than the focal length; at close focus the blur shares match at `k·N·(s − f)/(s − k·f)`.
|
|
8084
|
+
- Exposure uses the f-number on the lens. Equivalence never changes shutter, ISO or exposure.
|
|
8085
|
+
- Equal shares of the frame diagonal look the same when images are shown at the same size. `calculateBlurShareOfFrame()` does not judge whether a blur is visible.
|
|
8086
|
+
|
|
8087
|
+
See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-equivalence-f-number-area-and-blur-share).
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# camera/sensor-equivalence.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
|
+
## BlurShareOfFrame
|
|
6
|
+
|
|
7
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
export interface BlurShareOfFrame {
|
|
11
|
+
/** Blur-circle diameter on the sensor in millimetres. */
|
|
12
|
+
blurCircleMm: number;
|
|
13
|
+
/** Sensor diagonal in millimetres. */
|
|
14
|
+
frameDiagonalMm: number;
|
|
15
|
+
/** Blur diameter / frame diagonal. Equal shares look the same at the same output size. */
|
|
16
|
+
shareOfDiagonal: number;
|
|
17
|
+
/** Blur diameter / frame width. */
|
|
18
|
+
shareOfWidth: number;
|
|
19
|
+
/** Blur diameter / frame height. */
|
|
20
|
+
shareOfHeight: number;
|
|
21
|
+
/** Blur on the print; present only when a print size was given. */
|
|
22
|
+
print?: BlurShareOfPrint;
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## BlurShareOfPrint
|
|
27
|
+
|
|
28
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
export interface BlurShareOfPrint {
|
|
32
|
+
/** Linear enlargement from sensor to print: the smaller of the long-side and short-side ratios. */
|
|
33
|
+
enlargement: number;
|
|
34
|
+
/** Blur-circle diameter on the print in millimetres: blurCircleMm × enlargement. */
|
|
35
|
+
blurOnPrintMm: number;
|
|
36
|
+
/** Size of the printed image in millimetres (equal to the print when the aspect ratios match). */
|
|
37
|
+
imageLongSideMm: number;
|
|
38
|
+
imageShortSideMm: number;
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## calculateBlurShareOfFrame
|
|
43
|
+
|
|
44
|
+
A blur circle on the sensor as a share of the frame (diagonal, width and height) and, when a
|
|
45
|
+
print size is given, its diameter on that print.
|
|
46
|
+
|
|
47
|
+
Equal shares of the diagonal look the same when the images are shown at the same size, which
|
|
48
|
+
is how "same framing" comparisons across sensor sizes are stated. Every value is an exact ratio
|
|
49
|
+
or scaling; whether a blur is visible depends on viewing distance and acuity, which this
|
|
50
|
+
function does not judge.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
export function calculateBlurShareOfFrame(
|
|
54
|
+
input: CalculateBlurShareOfFrameInput
|
|
55
|
+
): CalculationResult<BlurShareOfFrame>;
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This call is synchronous; any supplied provider must follow its explicit synchronous contract.
|
|
59
|
+
|
|
60
|
+
## CalculateBlurShareOfFrameInput
|
|
61
|
+
|
|
62
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
export interface CalculateBlurShareOfFrameInput {
|
|
66
|
+
/** Blur-circle diameter on the sensor in millimetres (for example from calculateDefocusCircle). Zero is allowed. */
|
|
67
|
+
blurCircleMm: number;
|
|
68
|
+
/** Physical active imaging area of the sensor. */
|
|
69
|
+
sensor: SensorImagingArea;
|
|
70
|
+
/**
|
|
71
|
+
* Optional print size. The whole frame is enlarged to fit inside it without cropping, long side
|
|
72
|
+
* to long side.
|
|
73
|
+
*/
|
|
74
|
+
print?: { widthMm: number; heightMm: number };
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## calculateSensorEquivalence
|
|
79
|
+
|
|
80
|
+
Equivalence of a lens (focal length, f-number) on one sensor against a reference format,
|
|
81
|
+
36 × 24 mm by default: crop factor, equivalent focal length, equivalent f-number for depth of
|
|
82
|
+
field, blur and total light, the unchanged f-number for exposure, sensor area ratio, total-light
|
|
83
|
+
difference in stops and the angles of view.
|
|
84
|
+
|
|
85
|
+
Call it once per sensor for a lens table. Every quantity carries a `basis` entry saying whether it
|
|
86
|
+
is exact under its stated conditions or an approximation. The equivalent f-number is an
|
|
87
|
+
approximation (exact when the focus distance is much greater than the focal length) and never an
|
|
88
|
+
exposure setting: exposure uses `exposureFNumber`, the f-number on the lens.
|
|
89
|
+
|
|
90
|
+
Example: 50 mm f/2 on a 23.5 × 15.6 mm sensor gives crop factor ≈ 1.53, ≈ 77 mm and ≈ f/3.1.
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
export function calculateSensorEquivalence(
|
|
94
|
+
input: CalculateSensorEquivalenceInput
|
|
95
|
+
): CalculationResult<SensorEquivalence>;
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
This call is synchronous; any supplied provider must follow its explicit synchronous contract.
|
|
99
|
+
|
|
100
|
+
## CalculateSensorEquivalenceInput
|
|
101
|
+
|
|
102
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
export interface CalculateSensorEquivalenceInput {
|
|
106
|
+
/** Physical active imaging area of the sensor the lens is used on. */
|
|
107
|
+
sensor: SensorImagingArea;
|
|
108
|
+
/** Physical focal length of the lens in millimetres. */
|
|
109
|
+
focalLengthMm: number;
|
|
110
|
+
/** f-number set on the lens (N = focal length / entrance-pupil diameter). */
|
|
111
|
+
fNumber: number;
|
|
112
|
+
/** Format to compare against. Defaults to the 36 × 24 mm frame. */
|
|
113
|
+
reference?: SensorImagingArea;
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## EquivalenceExactness
|
|
118
|
+
|
|
119
|
+
Whether a value is exact under its stated conditions or an approximation.
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
export type EquivalenceExactness = "exact" | "approximation";
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## EquivalenceValueBasis
|
|
126
|
+
|
|
127
|
+
How one equivalence quantity was obtained, and under which conditions it holds.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
export interface EquivalenceValueBasis {
|
|
131
|
+
/** `"exact"` under the stated conditions, or `"approximation"`. */
|
|
132
|
+
kind: EquivalenceExactness;
|
|
133
|
+
/** Plain statement of the definition and its conditions. */
|
|
134
|
+
statement: string;
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## SensorEquivalence
|
|
139
|
+
|
|
140
|
+
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
export interface SensorEquivalence {
|
|
144
|
+
/** Physical focal length supplied by the caller, in millimetres. */
|
|
145
|
+
focalLengthMm: number;
|
|
146
|
+
/** f-number supplied by the caller. */
|
|
147
|
+
fNumber: number;
|
|
148
|
+
/** Sensor active imaging area used. */
|
|
149
|
+
sensor: SensorImagingArea;
|
|
150
|
+
/** Reference format used (36 × 24 mm unless the caller passed one). */
|
|
151
|
+
reference: SensorImagingArea;
|
|
152
|
+
/** Sensor diagonal in millimetres. */
|
|
153
|
+
sensorDiagonalMm: number;
|
|
154
|
+
/** Reference diagonal in millimetres. */
|
|
155
|
+
referenceDiagonalMm: number;
|
|
156
|
+
/** Diagonal crop factor: reference diagonal / sensor diagonal. Greater than 1 for a smaller sensor. */
|
|
157
|
+
cropFactor: number;
|
|
158
|
+
/** Focal length on the reference format with the same diagonal angle of view: cropFactor × focalLengthMm. */
|
|
159
|
+
equivalentFocalLengthMm: number;
|
|
160
|
+
/**
|
|
161
|
+
* f-number on the reference format that gives the same depth of field, background blur and total
|
|
162
|
+
* light for the same framing and output size: cropFactor × fNumber. Not an exposure setting.
|
|
163
|
+
*/
|
|
164
|
+
equivalentFNumber: number;
|
|
165
|
+
/** f-number that sets exposure (image brightness): the f-number on the lens, unchanged by sensor size. */
|
|
166
|
+
exposureFNumber: number;
|
|
167
|
+
/** Entrance-pupil diameter f/N in millimetres; the equivalent lens has the same diameter. */
|
|
168
|
+
entrancePupilDiameterMm: number;
|
|
169
|
+
/** Sensor area / reference area. Less than 1 for a smaller sensor. */
|
|
170
|
+
areaRatio: number;
|
|
171
|
+
/**
|
|
172
|
+
* log2(areaRatio): total light collected by the sensor relative to the reference at the same
|
|
173
|
+
* f-number, shutter time and scene, in stops. Negative for a smaller sensor.
|
|
174
|
+
*/
|
|
175
|
+
totalLightDifferenceStops: number;
|
|
176
|
+
/** Angles of view of this lens on the sensor (the equivalent lens gives the same diagonal angle). */
|
|
177
|
+
fieldOfView: SensorEquivalenceFieldOfView;
|
|
178
|
+
/** Exact or approximated, with the conditions, for every quantity above. */
|
|
179
|
+
basis: Readonly<Record<SensorEquivalenceQuantity, EquivalenceValueBasis>>;
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## SensorEquivalenceFieldOfView
|
|
184
|
+
|
|
185
|
+
Angles of view of an ideal rectilinear lens focused at infinity, in degrees.
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
export interface SensorEquivalenceFieldOfView {
|
|
189
|
+
horizontalDegrees: number;
|
|
190
|
+
verticalDegrees: number;
|
|
191
|
+
diagonalDegrees: number;
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
## SensorEquivalenceQuantity
|
|
196
|
+
|
|
197
|
+
Quantities of a sensor equivalence result that carry an exactness statement.
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
export type SensorEquivalenceQuantity =
|
|
201
|
+
| "cropFactor"
|
|
202
|
+
| "equivalentFocalLengthMm"
|
|
203
|
+
| "equivalentFNumber"
|
|
204
|
+
| "exposureFNumber"
|
|
205
|
+
| "entrancePupilDiameterMm"
|
|
206
|
+
| "areaRatio"
|
|
207
|
+
| "totalLightDifferenceStops"
|
|
208
|
+
| "fieldOfView";
|
|
209
|
+
```
|
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.19.0" as const
|
|
15
15
|
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# equipment/ideal-lens.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
|
+
## IDEAL_LENS_LIMITS
|
|
6
|
+
|
|
7
|
+
Ideal lens (#576): the bounds of `simulateCapture`'s and the display view's `lens.ideal`, a plain aberration-free thin lens
|
|
8
|
+
named by its physical focal length, on any sensor. Its blur is that of the options the request gives, the same models as with
|
|
9
|
+
`lens.prime`: `lens.defocus` (the thin-lens blur circle, `calculateDefocusCircle`), `lens.diffraction` (the Airy pattern of a
|
|
10
|
+
circular pupil) and `lens.vignetting` (a caller profile). Without them it renders sharp and evenly lit.
|
|
11
|
+
|
|
12
|
+
- `fNumber.minimum` 0.5 is a physical limit: a lens in air that satisfies the Abbe sine condition (aplanatic) has
|
|
13
|
+
N = 1/(2·n·sin θ′) with n = 1 and sin θ′ ≤ 1, so N ≥ 0.5 (the sine condition: Born & Wolf, *Principles of Optics*, 7th ed.;
|
|
14
|
+
Kingslake & Johnson, *Lens Design Fundamentals*, 2nd ed.). The engine's thin-lens defocus and scalar Airy diffraction
|
|
15
|
+
are paraxial models, so near f/0.5 they are approximations further from a real lens than at f/2.
|
|
16
|
+
- `fNumber.maximum` 128 and both focal-length bounds are engine bounds, not physical limits: 1 mm is shorter than phone
|
|
17
|
+
camera lenses, 4000 mm longer than photographic lenses, and f/128 beyond large-format lenses' smallest stops.
|
|
18
|
+
- The ideal lens's f-number range is the whole of `fNumber`: manual and aperture priority take any `lens.fNumber` in it, and
|
|
19
|
+
shutter priority and program may choose any aperture in it (in dim light, wider than real lenses open).
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
IDEAL_LENS_LIMITS = Object.freeze({
|
|
23
|
+
focalLengthMm: Object.freeze({ minimum: 1, maximum: 4000 }),
|
|
24
|
+
fNumber: Object.freeze({ minimum: 0.5, maximum: 128 })
|
|
25
|
+
})
|
|
26
|
+
```
|