@photivra/engine 2.12.0 → 2.13.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 +77 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +1 -0
- package/dist/capture/export-jpeg.d.ts +1 -1
- package/dist/capture/export-jpeg.js +6 -4
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
- package/dist/exposure/auto-iso-slowest-shutter.js +216 -0
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -0
- package/dist/exposure/exposure-mode-resolver.d.ts +57 -1
- package/dist/exposure/exposure-mode-resolver.js +238 -33
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/facade/diffraction-provider.js +12 -13
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +27 -0
- package/dist/facade/display-view.js +234 -65
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +93 -10
- package/dist/facade/simulate-capture.js +146 -22
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +2 -2
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +2 -1
- package/dist/gpu/display-view-quality-gpu.js +24 -14
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +13 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.js +36 -2
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +10 -0
- package/dist/optics/irradiance-convolution.js +48 -13
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/output/print-jpeg.js +12 -9
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/output/text-metadata.d.ts +22 -2
- package/dist/output/text-metadata.js +58 -9
- package/dist/output/text-metadata.js.map +1 -1
- package/docs/API_REFERENCE.md +21 -1
- package/docs/QUICK_START.md +65 -5
- package/docs/USAGE.md +207 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +67 -2
- package/docs/api/exposure-auto-iso-slowest-shutter.md +139 -0
- package/docs/api/exposure-exposure-mode-resolver.md +68 -0
- package/docs/api/facade-simulate-capture.md +90 -9
- package/docs/api/output-text-metadata.md +34 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EvidenceProvenance } from "../core/evidence-provenance.js";
|
|
2
2
|
import { type GenericEquipmentTier, type GenericEquipmentTierPreset } from "../equipment/generic-tier-assets.js";
|
|
3
|
-
import { type ExposureResolutionConstraint } from "../exposure/exposure-mode-resolver.js";
|
|
3
|
+
import { type AutoIsoLimits, type ExposureResolutionConstraint } from "../exposure/exposure-mode-resolver.js";
|
|
4
4
|
import { resolveColorTemperatureWhiteBalance, type ResolvedWhiteBalanceState } from "../color/white-balance.js";
|
|
5
5
|
import { type CaptureWhiteBalanceIntent } from "../capture/simulated-capture.js";
|
|
6
6
|
import { type NativeRawOutput, type NativeRawPlan } from "../capture/native-raw.js";
|
|
@@ -41,23 +41,81 @@ export type SimulateCaptureAutoIsoAfterMaximum = "allow-slower-shutter" | "hold-
|
|
|
41
41
|
export interface SimulateCaptureAutoIsoReport {
|
|
42
42
|
/** The ISO the meter target asked for at the resolved aperture and shutter, before the Auto ISO range and the ISO grid. */
|
|
43
43
|
idealIsoBeforeConstraints: number;
|
|
44
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* The body's Auto ISO range: its Auto ISO bounds, or its ISO range when it declares none (the tiers: ISO 100 to their
|
|
46
|
+
* maximum). Without `limits` the ISO was chosen in this range.
|
|
47
|
+
*/
|
|
45
48
|
autoIsoMinimum: number;
|
|
46
49
|
autoIsoMaximum: number;
|
|
50
|
+
/**
|
|
51
|
+
* #505: present only with `exposure.autoIso.limits`: the photographer's Auto ISO range the ISO was chosen in, inside the
|
|
52
|
+
* body's (`minimum` is the body's Auto ISO minimum when the option omits it).
|
|
53
|
+
*/
|
|
54
|
+
limits?: {
|
|
55
|
+
minimum: number;
|
|
56
|
+
maximum: number;
|
|
57
|
+
};
|
|
47
58
|
/** Whether the ISO was held at an end of the Auto ISO range. */
|
|
48
59
|
clamped: false | "minimum" | "maximum";
|
|
49
60
|
/**
|
|
50
61
|
* The constraint that limited the resolved exposure: the resolver's `targetResidual.limitingConstraint`, for example
|
|
51
|
-
* `"iso-maximum"` (the exposure stays under target at the Auto ISO maximum), `"iso-
|
|
52
|
-
* selectable ISO) or `"none"`
|
|
62
|
+
* `"iso-maximum"` (the exposure stays under target at the body's Auto ISO maximum), `"user-iso-maximum"` (#505: at the
|
|
63
|
+
* photographer's `limits.maximum`, below the body's), `"iso-grid-quantization"` (the nearest selectable ISO) or `"none"`
|
|
64
|
+
* (on target).
|
|
53
65
|
*/
|
|
54
66
|
limitingConstraint: ExposureResolutionConstraint;
|
|
55
67
|
/** The resolver's residual, log2(target / achieved exposure), in stops: positive when the exposure is under the meter target, negative when over it. */
|
|
56
68
|
residualStops: number;
|
|
57
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Aperture priority, and program with a slowest preferred shutter (#504): the policy and slowest preferred shutter used,
|
|
71
|
+
* and whether the slower-shutter fallback ran.
|
|
72
|
+
*/
|
|
58
73
|
afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
|
|
59
74
|
slowestPreferredShutterSeconds?: number;
|
|
60
75
|
afterMaximumIsoFallbackUsed?: boolean;
|
|
76
|
+
/** #504, program only: whether the program line's shutter was longer than the slowest preferred shutter and was held. */
|
|
77
|
+
slowestPreferredShutterApplied?: boolean;
|
|
78
|
+
/** #504: present only with `slowestPreferredShutterSeconds: "auto"`: how the engine derived it (`estimateAutoIsoSlowestShutter`). */
|
|
79
|
+
automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterReport;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* #504: the settings of `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`. Every field is optional; the defaults are
|
|
83
|
+
* labelled generic values, recorded as `default` assumptions.
|
|
84
|
+
*/
|
|
85
|
+
export interface SimulateCaptureAutomaticSlowestShutterOptions {
|
|
86
|
+
/** The bias in whole stops, as cameras' Auto minimum-shutter settings offer: +1 one stop faster, −1 one stop slower; −2 to +2. Default 0. */
|
|
87
|
+
fasterByStops?: number;
|
|
88
|
+
/** The shake blur allowed, in sensor pixels of `sensor.pixelPitchMicrometers`. Default 1 (a generic pixel-level budget). */
|
|
89
|
+
blurBudgetPixels?: number;
|
|
90
|
+
/**
|
|
91
|
+
* The hand's constant angular velocity, rad/s (signs as in `motion.angularVelocityRadPerSec`; only the magnitude matters).
|
|
92
|
+
* Default `GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC`: 4.39 °/s on yaw, a cited generic value for a 100 g phone held in
|
|
93
|
+
* two hands (Xiao et al., Proc. SPIE 7250, 2009), not a measurement of you or of this camera.
|
|
94
|
+
*/
|
|
95
|
+
angularVelocityRadPerSec?: {
|
|
96
|
+
yaw: number;
|
|
97
|
+
pitch: number;
|
|
98
|
+
};
|
|
99
|
+
/** Stabilization in equivalent stops (≥ 0, at most 10), scaling the motion by 2^−stops. Default 0: the tiers declare no stabilization. */
|
|
100
|
+
stabilizationStopsEquivalent?: number;
|
|
101
|
+
}
|
|
102
|
+
/** #504: the automatic slowest preferred shutter in the Auto ISO report. */
|
|
103
|
+
export interface SimulateCaptureAutomaticSlowestShutterReport {
|
|
104
|
+
/** The exposure time whose camera-shake blur equals the budget (`estimateCameraShakeBlur`), before the bias and the grid. */
|
|
105
|
+
derivedShutterSeconds: number;
|
|
106
|
+
fasterByStops: number;
|
|
107
|
+
/** derivedShutterSeconds · 2^−fasterByStops. */
|
|
108
|
+
biasedShutterSeconds: number;
|
|
109
|
+
/** The slowest preferred shutter used: the biased time within the body's shutter range. */
|
|
110
|
+
shutterSeconds: number;
|
|
111
|
+
blurBudgetPixels: number;
|
|
112
|
+
/** The shake blur at `shutterSeconds`, in pixels. */
|
|
113
|
+
blurAtShutterPixels: number;
|
|
114
|
+
angularVelocityRadPerSec: {
|
|
115
|
+
yaw: number;
|
|
116
|
+
pitch: number;
|
|
117
|
+
};
|
|
118
|
+
stabilizationStopsEquivalent: number;
|
|
61
119
|
}
|
|
62
120
|
export type SimulateCaptureOutputFormat = "srgb-8" | "srgb-16" | "jpeg" | "png" | "dng";
|
|
63
121
|
export type SimulateCaptureWhiteBalance = "scene-illuminant" | "auto" | "none" | {
|
|
@@ -233,13 +291,27 @@ export interface SimulateCaptureOptions {
|
|
|
233
291
|
*/
|
|
234
292
|
iso?: number | "auto";
|
|
235
293
|
/**
|
|
236
|
-
*
|
|
237
|
-
*
|
|
238
|
-
*
|
|
294
|
+
* Auto ISO settings, with `iso: "auto"` only.
|
|
295
|
+
*
|
|
296
|
+
* - `slowestPreferredShutterSeconds` and `afterMaximumIso` (#516): aperture priority, where the slowest shutter is
|
|
297
|
+
* required, and (#504) program, where it is optional. The shutter gets as long as `slowestPreferredShutterSeconds`
|
|
298
|
+
* (seconds, within the body's shutter range) at the lowest Auto ISO; beyond it the ISO rises; at the Auto ISO maximum
|
|
299
|
+
* `afterMaximumIso` decides (default `"allow-slower-shutter"`; in program the shutter then never gets longer than the
|
|
300
|
+
* program line's own). Without it, program follows its line as before.
|
|
301
|
+
* - `slowestPreferredShutterSeconds: "auto"` (#504): the engine derives it from the focal length, the pixel pitch and a
|
|
302
|
+
* hand-shake profile with `estimateAutoIsoSlowestShutter` (the camera-shake model, not the 1/focal-length rule of
|
|
303
|
+
* thumb); `automaticSlowestShutter` sets the bias, the blur budget, the shake and the stabilization.
|
|
304
|
+
* - `limits` (#505): every mode. The photographer's Auto ISO range `{ maximum, minimum? }`, a choice kept apart from
|
|
305
|
+
* the body's Auto ISO capability: both are selectable ISOs inside the body's Auto ISO range, `minimum` at most
|
|
306
|
+
* `maximum` (default: the body's minimum). Outside the body's range or off its ISO grid the request fails; the
|
|
307
|
+
* engine never clips a limit. When the ISO stops at a limit strictly inside the body's range, the report's
|
|
308
|
+
* `limitingConstraint` is `"user-iso-maximum"` or `"user-iso-minimum"`.
|
|
239
309
|
*/
|
|
240
310
|
autoIso?: {
|
|
241
|
-
slowestPreferredShutterSeconds
|
|
311
|
+
slowestPreferredShutterSeconds?: number | "auto";
|
|
242
312
|
afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
|
|
313
|
+
limits?: AutoIsoLimits;
|
|
314
|
+
automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterOptions;
|
|
243
315
|
};
|
|
244
316
|
/** Shutter for manual and shutter-priority modes. Default 1/125 s. */
|
|
245
317
|
shutterSeconds?: number;
|
|
@@ -490,6 +562,13 @@ export interface SimulateCaptureResult {
|
|
|
490
562
|
* the metered luminance is 0 (a black scene has no standard exposure).
|
|
491
563
|
*/
|
|
492
564
|
standardExposureDeviationStops: number | null;
|
|
565
|
+
/**
|
|
566
|
+
* The illuminance, lux, at which the settings used give the standard exposure: E·L_ref(N, t, S)/L = E·2^Δ, with E the
|
|
567
|
+
* request's `illuminanceLux`. Exact for a reflective scene (a chart, a reflectance function or a material scene), whose
|
|
568
|
+
* metered luminance is proportional to E. Null for a black scene (L = 0) and for a radiance-function scene, whose
|
|
569
|
+
* radiance does not depend on the illuminance. It may exceed the 1e7 lux that `illuminanceLux` accepts.
|
|
570
|
+
*/
|
|
571
|
+
standardExposureIlluminanceLux: number | null;
|
|
493
572
|
/** Full output of the existing exposure-mode resolver. */
|
|
494
573
|
resolution: unknown;
|
|
495
574
|
};
|
|
@@ -761,7 +840,7 @@ export interface DisplayViewResult {
|
|
|
761
840
|
height: number;
|
|
762
841
|
};
|
|
763
842
|
};
|
|
764
|
-
/** The resolved exposure of the capture this view shows; `standardExposureDeviationStops` as in {@link SimulateCaptureResult}. */
|
|
843
|
+
/** The resolved exposure of the capture this view shows; `standardExposureDeviationStops` and `standardExposureIlluminanceLux` as in {@link SimulateCaptureResult}. */
|
|
765
844
|
exposure: {
|
|
766
845
|
mode: SimulateCaptureExposureMode;
|
|
767
846
|
settings: {
|
|
@@ -771,6 +850,8 @@ export interface DisplayViewResult {
|
|
|
771
850
|
};
|
|
772
851
|
ev100: number;
|
|
773
852
|
standardExposureDeviationStops: number | null;
|
|
853
|
+
/** As in the capture result. */
|
|
854
|
+
standardExposureIlluminanceLux: number | null;
|
|
774
855
|
/** #516: as in the capture result. */
|
|
775
856
|
isoControl: "manual" | "automatic";
|
|
776
857
|
autoIso?: SimulateCaptureAutoIsoReport;
|
|
@@ -882,6 +963,8 @@ export interface DisplayViewRunContext {
|
|
|
882
963
|
};
|
|
883
964
|
/** #515: the capture's `exposure.standardExposureDeviationStops`. */
|
|
884
965
|
standardExposureDeviationStops: number | null;
|
|
966
|
+
/** The capture's `exposure.standardExposureIlluminanceLux`. */
|
|
967
|
+
standardExposureIlluminanceLux: number | null;
|
|
885
968
|
sceneOption: unknown;
|
|
886
969
|
scenePreset: "composite" | "uniform" | "patches" | "slanted-edge" | "siemens-star" | "step-wedge" | "grey-ramp" | "checkerboard" | "point-grid" | "dead-leaves" | "bar-groups" | "hyperbolic-wedge" | "zone-plate" | "slanted-squares" | "line-grid" | "fiducial" | "reflectance-function" | "radiance-function" | "material-function";
|
|
887
970
|
shutterSeconds: number;
|
|
@@ -41,6 +41,7 @@ import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
|
41
41
|
import { meterRelativeExposure } from "../exposure/metering.js";
|
|
42
42
|
import { createExposureMeterTargetFromMeteringResult, setExposureCompensationOnMeterTarget } from "../exposure/metering-target.js";
|
|
43
43
|
import { resolveManualExposureMode, resolveAperturePriorityExposureMode, resolveAperturePriorityAutoIsoExposureMode, resolveShutterPriorityExposureMode, resolveProgramAutoExposureMode } from "../exposure/exposure-mode-resolver.js";
|
|
44
|
+
import { estimateAutoIsoSlowestShutter, GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC } from "../exposure/auto-iso-slowest-shutter.js";
|
|
44
45
|
import { parseExposureProgramLineProfile } from "../exposure/program-line.js";
|
|
45
46
|
import { resolveCaptureGeometry } from "../output/capture-geometry.js";
|
|
46
47
|
import { estimateCie1931ColorMatchingFunctions } from "../color/color-matching-functions.js";
|
|
@@ -137,6 +138,21 @@ function standardExposureDeviationStops(fNumber, shutterSeconds, iso, meteredLum
|
|
|
137
138
|
meteredLuminance + " cd/m²).");
|
|
138
139
|
return deviation;
|
|
139
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* The illuminance at which the settings give the standard exposure, E·L_ref/L (= E·2^Δ). A reflective scene's radiance is
|
|
143
|
+
* reflectance × k(E)·S(λ)/π with k linear in E (calculateIlluminantIrradianceScale), so the metered L is proportional to E
|
|
144
|
+
* and re-metering at this illuminance gives Δ = 0 up to rounding. A radiance-function scene ignores E, and a black scene
|
|
145
|
+
* has no standard exposure: null.
|
|
146
|
+
*/
|
|
147
|
+
function standardExposureIlluminanceLux(fNumber, shutterSeconds, iso, meteredLuminance, illuminanceLux, radianceScalesWithIlluminance) {
|
|
148
|
+
if (!radianceScalesWithIlluminance || !(meteredLuminance > 0))
|
|
149
|
+
return null;
|
|
150
|
+
const lux = illuminanceLux * sosReferenceLuminance(fNumber, shutterSeconds, iso) / meteredLuminance;
|
|
151
|
+
if (!Number.isFinite(lux))
|
|
152
|
+
throw new InvalidScientificResultError("exposure.standardExposureIlluminanceLux is not finite (metered scene luminance " +
|
|
153
|
+
meteredLuminance + " cd/m²).");
|
|
154
|
+
return lux;
|
|
155
|
+
}
|
|
140
156
|
/**
|
|
141
157
|
* Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
142
158
|
* output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
|
|
@@ -412,16 +428,87 @@ export function bilinearPhaseProfiles() {
|
|
|
412
428
|
* #516: the result's Auto ISO report, read from a resolved Auto ISO resolution (every mode's resolved Auto ISO branch carries
|
|
413
429
|
* `idealIsoBeforeConstraints`, `isoResolution` and a resolved `targetResidual`; aperture priority adds its fallback flag).
|
|
414
430
|
*/
|
|
415
|
-
function autoIsoReport(resolution, iso,
|
|
431
|
+
function autoIsoReport(resolution, iso, shutterPolicy, limits, automatic) {
|
|
416
432
|
const r = resolution;
|
|
433
|
+
// Program reports its hold under slowestPreferredShutter (#504); aperture priority at the top level.
|
|
434
|
+
const program = r.slowestPreferredShutter;
|
|
417
435
|
const ideal = r.idealIsoBeforeConstraints, residual = r.targetResidual?.residualStops, clamped = r.isoResolution?.clamped;
|
|
418
436
|
if (typeof ideal !== "number" || !Number.isFinite(ideal) || typeof residual !== "number" || !Number.isFinite(residual) || r.targetResidual?.status !== "resolved" ||
|
|
419
437
|
(clamped !== false && clamped !== "minimum" && clamped !== "maximum"))
|
|
420
438
|
throw new InvalidScientificResultError("Auto ISO resolution did not report a finite ideal ISO and residual.");
|
|
421
439
|
// The resolvers' Auto ISO range: the body's Auto ISO bounds, else its ISO range.
|
|
422
440
|
return { idealIsoBeforeConstraints: ideal, autoIsoMinimum: iso.autoIsoMinimum ?? iso.minimum, autoIsoMaximum: iso.autoIsoMaximum ?? iso.maximum, clamped,
|
|
423
|
-
limitingConstraint: r.targetResidual.limitingConstraint, residualStops: residual,
|
|
424
|
-
...(
|
|
441
|
+
...(limits ? { limits: { ...limits } } : {}), limitingConstraint: r.targetResidual.limitingConstraint, residualStops: residual,
|
|
442
|
+
...(shutterPolicy ? { ...shutterPolicy, afterMaximumIsoFallbackUsed: (program ? program.afterMaximumIsoFallbackUsed : r.afterMaximumIsoFallbackUsed) === true } : {}),
|
|
443
|
+
...(program ? { slowestPreferredShutterApplied: program.applied === true } : {}),
|
|
444
|
+
...(automatic ? { automaticSlowestShutter: automatic } : {}) };
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* #505: `exposure.autoIso.limits`, the photographer's Auto ISO range. Both limits are selectable ISOs inside the body's
|
|
448
|
+
* Auto ISO range, `minimum` at most `maximum`; anything else fails (the resolvers check the same, but an option defect
|
|
449
|
+
* is a configuration error here). `minimum` defaults to the body's Auto ISO minimum.
|
|
450
|
+
*/
|
|
451
|
+
function parseAutoIsoLimitsOption(value, iso) {
|
|
452
|
+
const record = allowKeys(value, "exposure.autoIso.limits", ["maximum", "minimum"]);
|
|
453
|
+
if (record.maximum === undefined)
|
|
454
|
+
fail("exposure.autoIso.limits.maximum is required: the highest ISO Auto ISO may choose.");
|
|
455
|
+
const bodyMinimum = iso.autoIsoMinimum ?? iso.minimum, bodyMaximum = iso.autoIsoMaximum ?? iso.maximum;
|
|
456
|
+
const limit = (field) => {
|
|
457
|
+
const v = positive(record[field], "exposure.autoIso.limits." + field);
|
|
458
|
+
if (v < bodyMinimum || v > bodyMaximum)
|
|
459
|
+
fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be within the body's Auto ISO range, ISO " + bodyMinimum + " to " + bodyMaximum + ".");
|
|
460
|
+
if (iso.settingGrid.kind === "discrete-values" && !iso.settingGrid.values.includes(v))
|
|
461
|
+
fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be a selectable ISO of the body: " + iso.settingGrid.values.join(", ") + ".");
|
|
462
|
+
return v;
|
|
463
|
+
};
|
|
464
|
+
const maximum = limit("maximum"), minimum = record.minimum === undefined ? bodyMinimum : limit("minimum");
|
|
465
|
+
if (minimum > maximum)
|
|
466
|
+
fail("exposure.autoIso.limits.minimum (ISO " + minimum + ") must not exceed exposure.autoIso.limits.maximum (ISO " + maximum + ").");
|
|
467
|
+
return { minimum, maximum };
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* #504: `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`: the slowest preferred shutter from the camera-shake model
|
|
471
|
+
* (`estimateAutoIsoSlowestShutter`) for this lens, focus and pixel pitch, within the body's shutter range. Option defects are
|
|
472
|
+
* configuration errors; the defaults are recorded as `default` assumptions and the derivation as a `model` assumption.
|
|
473
|
+
*/
|
|
474
|
+
function resolveAutomaticSlowestShutterOption(value, context) {
|
|
475
|
+
const path = "exposure.autoIso.automaticSlowestShutter";
|
|
476
|
+
const record = allowKeys(value, path, ["fasterByStops", "blurBudgetPixels", "angularVelocityRadPerSec", "stabilizationStopsEquivalent"]);
|
|
477
|
+
const fasterByStops = context.pick(record, "fasterByStops", path + ".fasterByStops", 0, "No bias: the derived slowest shutter is used as is.");
|
|
478
|
+
if (typeof fasterByStops !== "number" || !Number.isInteger(fasterByStops) || Math.abs(fasterByStops) > 2)
|
|
479
|
+
fail(path + ".fasterByStops must be a whole number of stops from -2 to +2.");
|
|
480
|
+
const blurBudgetPixels = positive(context.pick(record, "blurBudgetPixels", path + ".blurBudgetPixels", 1, "A generic pixel-level blur budget: the shake trace during the exposure may be one pixel long. A choice, not a measured threshold."), path + ".blurBudgetPixels");
|
|
481
|
+
const angularOption = context.pick(record, "angularVelocityRadPerSec", path + ".angularVelocityRadPerSec", { ...GENERIC_HANDHELD_ANGULAR_VELOCITY_RAD_PER_SEC }, "Generic hand shake: 4.39 °/s (0.0766 rad/s) on yaw, the linear camera-motion rate Xiao, Farrell, Catrysse and Wandell (Proc. SPIE 7250, 2009) use for an average user " +
|
|
482
|
+
"holding a 100 g mobile handset with two hands. A labelled generic default, not a measurement of this photographer or camera.");
|
|
483
|
+
const angular = allowKeys(angularOption, path + ".angularVelocityRadPerSec", ["yaw", "pitch"]);
|
|
484
|
+
for (const axis of ["yaw", "pitch"]) {
|
|
485
|
+
const v = angular[axis];
|
|
486
|
+
if (typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > 100)
|
|
487
|
+
fail(path + ".angularVelocityRadPerSec." + axis + " must be a finite number within ±100 rad/s.");
|
|
488
|
+
}
|
|
489
|
+
const angularVelocityRadPerSec = { yaw: angular.yaw, pitch: angular.pitch };
|
|
490
|
+
if (angularVelocityRadPerSec.yaw === 0 && angularVelocityRadPerSec.pitch === 0)
|
|
491
|
+
fail(path + ".angularVelocityRadPerSec must be non-zero on at least one axis: without shake no slowest shutter follows.");
|
|
492
|
+
const stabilizationStopsEquivalent = context.pick(record, "stabilizationStopsEquivalent", path + ".stabilizationStopsEquivalent", 0, "No stabilization: the generic tiers declare none.");
|
|
493
|
+
if (typeof stabilizationStopsEquivalent !== "number" || !Number.isFinite(stabilizationStopsEquivalent) || stabilizationStopsEquivalent < 0 || stabilizationStopsEquivalent > 10)
|
|
494
|
+
fail(path + ".stabilizationStopsEquivalent must be a number of stops from 0 to 10.");
|
|
495
|
+
let estimate;
|
|
496
|
+
try {
|
|
497
|
+
estimate = estimateAutoIsoSlowestShutter({ focalLengthMm: context.focalLengthMm, focusDistanceM: context.focusDistanceM, angularVelocityRadPerSec,
|
|
498
|
+
stabilizationStopsEquivalent, blurBudget: { kind: "pixels", pixels: blurBudgetPixels, pixelPitchMicrometers: context.pitch }, fasterByStops,
|
|
499
|
+
shutterCapability: context.shutter }).value;
|
|
500
|
+
}
|
|
501
|
+
catch (error) {
|
|
502
|
+
fail(path + ": " + (error instanceof Error ? error.message : String(error)));
|
|
503
|
+
}
|
|
504
|
+
const report = { derivedShutterSeconds: estimate.derivedShutterSeconds, fasterByStops,
|
|
505
|
+
biasedShutterSeconds: estimate.biasedShutterSeconds, shutterSeconds: estimate.shutterSeconds, blurBudgetPixels, blurAtShutterPixels: estimate.blurAtShutterPixels,
|
|
506
|
+
angularVelocityRadPerSec, stabilizationStopsEquivalent };
|
|
507
|
+
context.assume("exposure.autoIso.automaticSlowestShutter", "model", { ...report, angularVelocityRadPerSec: { ...angularVelocityRadPerSec },
|
|
508
|
+
focalLengthMm: context.focalLengthMm, focusDistanceM: context.focusDistanceM, pixelPitchMicrometers: context.pitch }, "Automatic slowest shutter (#504): the exposure time at which estimateCameraShakeBlur()'s on-axis blur (constant angular velocity, thin-lens projection at the focus " +
|
|
509
|
+
"distance, stabilization as 2^-stops) equals the blur budget, times 2^-fasterByStops, held within the body's shutter range. An approximation; the 1/focal-length " +
|
|
510
|
+
"reciprocal rule is a rule of thumb and is not used.");
|
|
511
|
+
return report;
|
|
425
512
|
}
|
|
426
513
|
function defaultProgramLine() {
|
|
427
514
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
@@ -1227,8 +1314,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1227
1314
|
if (typeof isoOption === "string" && !automaticIso)
|
|
1228
1315
|
fail('exposure.iso must be a positive finite number or "auto".');
|
|
1229
1316
|
const iso = automaticIso ? NaN : positive(isoOption, "exposure.iso");
|
|
1230
|
-
if (exposureOptions.autoIso !== undefined && !
|
|
1231
|
-
fail('exposure.autoIso applies only
|
|
1317
|
+
if (exposureOptions.autoIso !== undefined && !automaticIso)
|
|
1318
|
+
fail('exposure.autoIso applies only with exposure.iso "auto"; omit it.');
|
|
1232
1319
|
const compensation = exposureOptions.compensationStops === undefined ? DEFAULTS.compensationStops : exposureOptions.compensationStops;
|
|
1233
1320
|
if (typeof compensation !== "number" || !Number.isFinite(compensation) || Math.abs(compensation) > 10)
|
|
1234
1321
|
fail("exposure.compensationStops must be within ±10.");
|
|
@@ -1279,31 +1366,65 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1279
1366
|
// #516: the resolvers' Auto ISO policies; their baseline is the lowest selectable ISO in the body's Auto ISO range.
|
|
1280
1367
|
const autoIsoBaseline = { isoBaseline: "minimum-selectable", isoQuantizationPolicy: "nearest-log2-lower-on-tie" };
|
|
1281
1368
|
let autoIsoAperturePriority;
|
|
1369
|
+
// #505: the photographer's Auto ISO limits, validated against the body's Auto ISO range; absent unless given.
|
|
1370
|
+
let autoIsoOptions = {};
|
|
1371
|
+
let autoIsoLimits;
|
|
1372
|
+
// #504: the slowest preferred shutter (aperture priority; optional in program) and, with "auto", how it was derived.
|
|
1373
|
+
let autoIsoShutterPolicy;
|
|
1374
|
+
let automaticSlowestShutter;
|
|
1282
1375
|
if (automaticIso) {
|
|
1283
1376
|
if (capabilities.iso.autoIsoAvailability !== "supported")
|
|
1284
1377
|
fail('exposure.iso "auto" needs a body with Auto ISO; this camera\'s Auto ISO is ' + capabilities.iso.autoIsoAvailability + ".");
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1378
|
+
autoIsoOptions = allowKeys(exposureOptions.autoIso, "exposure.autoIso", ["slowestPreferredShutterSeconds", "afterMaximumIso", "limits", "automaticSlowestShutter"]);
|
|
1379
|
+
if (mode !== "aperture-priority" && mode !== "program")
|
|
1380
|
+
for (const key of ["slowestPreferredShutterSeconds", "afterMaximumIso", "automaticSlowestShutter"])
|
|
1381
|
+
if (autoIsoOptions[key] !== undefined)
|
|
1382
|
+
fail("exposure.autoIso." + key + ' applies only to exposure.mode "aperture-priority" or "program"; omit it.');
|
|
1383
|
+
if (autoIsoOptions.limits !== undefined)
|
|
1384
|
+
autoIsoLimits = parseAutoIsoLimitsOption(autoIsoOptions.limits, capabilities.iso);
|
|
1385
|
+
assume("exposure.autoIso", "model", { minimum: capabilities.iso.autoIsoMinimum ?? capabilities.iso.minimum, maximum: capabilities.iso.autoIsoMaximum ?? capabilities.iso.maximum,
|
|
1386
|
+
...(autoIsoLimits ? { limits: { ...autoIsoLimits } } : {}), ...autoIsoBaseline }, "Auto ISO: the engine's exposure-mode resolver chooses the ISO from the meter target, starting from the lowest selectable ISO in the body's Auto ISO range and " +
|
|
1387
|
+
"snapping to the nearest selectable ISO in log2 (the lower on a tie), held at the range's ends. A generic policy, not a manufacturer's Auto ISO." +
|
|
1388
|
+
(autoIsoLimits ? " The photographer's limits (exposure.autoIso.limits, #505) narrow that range to ISO " + autoIsoLimits.minimum + " to " + autoIsoLimits.maximum + "." : ""));
|
|
1389
|
+
}
|
|
1390
|
+
if (automaticIso && mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds === undefined) {
|
|
1391
|
+
for (const key of ["afterMaximumIso", "automaticSlowestShutter"])
|
|
1392
|
+
if (autoIsoOptions[key] !== undefined)
|
|
1393
|
+
fail("exposure.autoIso." + key + ' in program needs exposure.autoIso.slowestPreferredShutterSeconds; omit it or set the slowest shutter.');
|
|
1394
|
+
}
|
|
1395
|
+
if (automaticIso && (mode === "aperture-priority" || (mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds !== undefined))) {
|
|
1290
1396
|
if (autoIsoOptions.slowestPreferredShutterSeconds === undefined)
|
|
1291
|
-
fail('exposure.autoIso.slowestPreferredShutterSeconds is required with exposure.mode "aperture-priority" and exposure.iso "auto": the slowest shutter, in seconds, before Auto ISO raises the ISO.');
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1397
|
+
fail('exposure.autoIso.slowestPreferredShutterSeconds is required with exposure.mode "aperture-priority" and exposure.iso "auto": the slowest shutter, in seconds, before Auto ISO raises the ISO, or "auto".');
|
|
1398
|
+
if (autoIsoOptions.automaticSlowestShutter !== undefined && autoIsoOptions.slowestPreferredShutterSeconds !== "auto")
|
|
1399
|
+
fail('exposure.autoIso.automaticSlowestShutter applies only with exposure.autoIso.slowestPreferredShutterSeconds "auto"; omit it.');
|
|
1400
|
+
let slowestPreferredShutterSeconds;
|
|
1401
|
+
if (autoIsoOptions.slowestPreferredShutterSeconds === "auto") {
|
|
1402
|
+
automaticSlowestShutter = resolveAutomaticSlowestShutterOption(autoIsoOptions.automaticSlowestShutter, { focalLengthMm, focusDistanceM, pitch,
|
|
1403
|
+
shutter: capabilities.shutter, assume, pick });
|
|
1404
|
+
slowestPreferredShutterSeconds = automaticSlowestShutter.shutterSeconds;
|
|
1405
|
+
}
|
|
1406
|
+
else {
|
|
1407
|
+
if (typeof autoIsoOptions.slowestPreferredShutterSeconds === "string")
|
|
1408
|
+
fail('exposure.autoIso.slowestPreferredShutterSeconds must be a positive number of seconds or "auto".');
|
|
1409
|
+
slowestPreferredShutterSeconds = positive(autoIsoOptions.slowestPreferredShutterSeconds, "exposure.autoIso.slowestPreferredShutterSeconds");
|
|
1410
|
+
if (slowestPreferredShutterSeconds < capabilities.shutter.minimumSeconds || slowestPreferredShutterSeconds > capabilities.shutter.maximumSeconds)
|
|
1411
|
+
fail("exposure.autoIso.slowestPreferredShutterSeconds must be within the body's shutter range, " + capabilities.shutter.minimumSeconds + " s to " + capabilities.shutter.maximumSeconds + " s.");
|
|
1412
|
+
}
|
|
1295
1413
|
const afterMaximumIso = pick(autoIsoOptions, "afterMaximumIso", "exposure.autoIso.afterMaximumIso", "allow-slower-shutter", "At the Auto ISO maximum, the shutter may get slower than the slowest preferred shutter to reach the meter target.");
|
|
1296
1414
|
if (afterMaximumIso !== "allow-slower-shutter" && afterMaximumIso !== "hold-preferred-shutter")
|
|
1297
1415
|
fail('exposure.autoIso.afterMaximumIso must be "allow-slower-shutter" or "hold-preferred-shutter".');
|
|
1298
|
-
|
|
1416
|
+
autoIsoShutterPolicy = { slowestPreferredShutterSeconds, afterMaximumIso };
|
|
1417
|
+
if (mode === "aperture-priority")
|
|
1418
|
+
autoIsoAperturePriority = autoIsoShutterPolicy;
|
|
1299
1419
|
}
|
|
1420
|
+
const limitsPolicy = autoIsoLimits ? { autoIsoLimits: { ...autoIsoLimits } } : {};
|
|
1300
1421
|
if (mode === "manual") {
|
|
1301
1422
|
resolution = resolveManualExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
|
|
1302
|
-
isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy } : { kind: "manual", iso } });
|
|
1423
|
+
isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy, ...limitsPolicy } : { kind: "manual", iso } });
|
|
1303
1424
|
}
|
|
1304
1425
|
else if (autoIsoAperturePriority) {
|
|
1305
1426
|
resolution = resolveAperturePriorityAutoIsoExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber,
|
|
1306
|
-
policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority } });
|
|
1427
|
+
policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority, ...limitsPolicy } });
|
|
1307
1428
|
}
|
|
1308
1429
|
else if (mode === "aperture-priority") {
|
|
1309
1430
|
resolution = resolveAperturePriorityExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
|
|
@@ -1311,13 +1432,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1311
1432
|
}
|
|
1312
1433
|
else if (mode === "shutter-priority") {
|
|
1313
1434
|
resolution = resolveShutterPriorityExposureMode({ target, capabilities, referenceExposure: reference, manualShutterSeconds: shutterSeconds,
|
|
1314
|
-
isoControl: automaticIso ? { kind: "automatic", policy: { kind: "minimum-iso-aperture-first", apertureSelectionPolicy: "not-wider-than-target", ...autoIsoBaseline } } :
|
|
1435
|
+
isoControl: automaticIso ? { kind: "automatic", policy: { kind: "minimum-iso-aperture-first", apertureSelectionPolicy: "not-wider-than-target", ...autoIsoBaseline, ...limitsPolicy } } :
|
|
1315
1436
|
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1316
1437
|
}
|
|
1317
1438
|
else {
|
|
1318
1439
|
assume("exposure.programLine", "model", "facade-generic-program-line", "Engine-authored generic program line (f/16 1/2000 s … f/8 1/125 s … f/4 1.024 s); not a manufacturer program.");
|
|
1319
1440
|
resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(),
|
|
1320
|
-
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline
|
|
1441
|
+
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1442
|
+
{ kind: "manual", iso } });
|
|
1321
1443
|
}
|
|
1322
1444
|
if (resolution.status !== "resolved" || !resolution.resolvedSettings)
|
|
1323
1445
|
fail("Exposure could not be resolved (" + resolution.status + "); the scene may have no signal.");
|
|
@@ -1326,8 +1448,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1326
1448
|
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
1327
1449
|
if (!Number.isFinite(resolvedIso) || resolvedIso <= 0)
|
|
1328
1450
|
fail("Exposure could not be resolved: the resolved ISO is not a positive finite number.");
|
|
1329
|
-
const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution, capabilities.iso,
|
|
1451
|
+
const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution, capabilities.iso, autoIsoShutterPolicy, autoIsoLimits, automaticSlowestShutter) } : { isoControl: "manual" };
|
|
1330
1452
|
const exposureDeviationStops = standardExposureDeviationStops(fNumber, shutterSeconds, resolvedIso, meteredLuminance);
|
|
1453
|
+
const exposureIlluminanceLux = standardExposureIlluminanceLux(fNumber, shutterSeconds, resolvedIso, meteredLuminance, illuminanceLux, callerScene?.kind !== "radiance-function");
|
|
1331
1454
|
const defocusDisc = defocusModel === undefined ? undefined :
|
|
1332
1455
|
resolveDefocusOption({ run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch, diffraction: diffractionRadiusOption !== undefined });
|
|
1333
1456
|
// #492: the capture applies the disc as an object-space convolution, which needs every disc point a site sees to be another site's scene point.
|
|
@@ -1768,7 +1891,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1768
1891
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1769
1892
|
// that only captures does not contain the display view.
|
|
1770
1893
|
if (displayView)
|
|
1771
|
-
return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1894
|
+
return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, standardExposureIlluminanceLux: exposureIlluminanceLux, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1772
1895
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1773
1896
|
if (idealReference) {
|
|
1774
1897
|
const referenceStarted = clockMs();
|
|
@@ -2132,7 +2255,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2132
2255
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2133
2256
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
2134
2257
|
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
|
|
2135
|
-
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2258
|
+
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2259
|
+
standardExposureIlluminanceLux: exposureIlluminanceLux, resolution },
|
|
2136
2260
|
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
2137
2261
|
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
2138
2262
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|