@photivra/engine 2.11.1 → 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 +94 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +2 -4
- 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 +159 -3
- package/dist/facade/simulate-capture.js +203 -10
- 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 +23 -1
- package/docs/QUICK_START.md +109 -3
- package/docs/USAGE.md +234 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +77 -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 +153 -5
- package/docs/api/output-text-metadata.md +34 -0
- package/package.json +1 -1
|
@@ -1,5 +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 AutoIsoLimits, type ExposureResolutionConstraint } from "../exposure/exposure-mode-resolver.js";
|
|
3
4
|
import { resolveColorTemperatureWhiteBalance, type ResolvedWhiteBalanceState } from "../color/white-balance.js";
|
|
4
5
|
import { type CaptureWhiteBalanceIntent } from "../capture/simulated-capture.js";
|
|
5
6
|
import { type NativeRawOutput, type NativeRawPlan } from "../capture/native-raw.js";
|
|
@@ -27,6 +28,95 @@ import { type ChartSizeOption, type CaptureIlluminant, type CallerMaterialScene,
|
|
|
27
28
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
28
29
|
export declare const SIMULATE_CAPTURE_FACADE_VERSION: "1.0.0";
|
|
29
30
|
export type SimulateCaptureExposureMode = "manual" | "aperture-priority" | "shutter-priority" | "program";
|
|
31
|
+
/**
|
|
32
|
+
* #516: what aperture priority does with Auto ISO once the ISO reaches the body's Auto ISO maximum and the exposure is
|
|
33
|
+
* still under the meter target: `"allow-slower-shutter"` lengthens the shutter past the slowest preferred shutter to reach
|
|
34
|
+
* the target; `"hold-preferred-shutter"` keeps the preferred shutter and leaves the exposure under target.
|
|
35
|
+
*/
|
|
36
|
+
export type SimulateCaptureAutoIsoAfterMaximum = "allow-slower-shutter" | "hold-preferred-shutter";
|
|
37
|
+
/**
|
|
38
|
+
* #516: how Auto ISO chose the ISO (`exposure.iso: "auto"`), reported in the result's `exposure.autoIso`. The full
|
|
39
|
+
* resolver output stays in the capture's `exposure.resolution`.
|
|
40
|
+
*/
|
|
41
|
+
export interface SimulateCaptureAutoIsoReport {
|
|
42
|
+
/** The ISO the meter target asked for at the resolved aperture and shutter, before the Auto ISO range and the ISO grid. */
|
|
43
|
+
idealIsoBeforeConstraints: number;
|
|
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
|
+
*/
|
|
48
|
+
autoIsoMinimum: number;
|
|
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
|
+
};
|
|
58
|
+
/** Whether the ISO was held at an end of the Auto ISO range. */
|
|
59
|
+
clamped: false | "minimum" | "maximum";
|
|
60
|
+
/**
|
|
61
|
+
* The constraint that limited the resolved exposure: the resolver's `targetResidual.limitingConstraint`, for example
|
|
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).
|
|
65
|
+
*/
|
|
66
|
+
limitingConstraint: ExposureResolutionConstraint;
|
|
67
|
+
/** The resolver's residual, log2(target / achieved exposure), in stops: positive when the exposure is under the meter target, negative when over it. */
|
|
68
|
+
residualStops: number;
|
|
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
|
+
*/
|
|
73
|
+
afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
|
|
74
|
+
slowestPreferredShutterSeconds?: number;
|
|
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;
|
|
119
|
+
}
|
|
30
120
|
export type SimulateCaptureOutputFormat = "srgb-8" | "srgb-16" | "jpeg" | "png" | "dng";
|
|
31
121
|
export type SimulateCaptureWhiteBalance = "scene-illuminant" | "auto" | "none" | {
|
|
32
122
|
kelvin: number;
|
|
@@ -191,8 +281,38 @@ export interface SimulateCaptureOptions {
|
|
|
191
281
|
exposure?: {
|
|
192
282
|
/** Default "manual". */
|
|
193
283
|
mode?: SimulateCaptureExposureMode;
|
|
194
|
-
/**
|
|
195
|
-
|
|
284
|
+
/**
|
|
285
|
+
* Default 100. #516: `"auto"` lets the body's Auto ISO choose the ISO from the meter target, within its Auto ISO range
|
|
286
|
+
* (the tiers: ISO 100 to their maximum), in every mode: manual keeps the aperture and shutter; aperture priority keeps the
|
|
287
|
+
* aperture and follows `autoIso`; shutter priority picks the aperture at the lowest Auto ISO (no wider than the target
|
|
288
|
+
* needs, so at most the widest) and raises the ISO for the rest; program picks the program line at the lowest Auto ISO and
|
|
289
|
+
* raises the ISO beyond the line's end. The result reports the ISO in `exposure.settings.iso` and how it was chosen in
|
|
290
|
+
* `exposure.autoIso`.
|
|
291
|
+
*/
|
|
292
|
+
iso?: number | "auto";
|
|
293
|
+
/**
|
|
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"`.
|
|
309
|
+
*/
|
|
310
|
+
autoIso?: {
|
|
311
|
+
slowestPreferredShutterSeconds?: number | "auto";
|
|
312
|
+
afterMaximumIso?: SimulateCaptureAutoIsoAfterMaximum;
|
|
313
|
+
limits?: AutoIsoLimits;
|
|
314
|
+
automaticSlowestShutter?: SimulateCaptureAutomaticSlowestShutterOptions;
|
|
315
|
+
};
|
|
196
316
|
/** Shutter for manual and shutter-priority modes. Default 1/125 s. */
|
|
197
317
|
shutterSeconds?: number;
|
|
198
318
|
/** Exposure compensation applied to the meter target, in stops. Default 0. */
|
|
@@ -430,7 +550,25 @@ export interface SimulateCaptureResult {
|
|
|
430
550
|
};
|
|
431
551
|
/** Exposure value at ISO 100 of the settings, log2(N²/t). */
|
|
432
552
|
ev100: number;
|
|
553
|
+
/** #516: `"automatic"` when the request asked for `iso: "auto"`, else `"manual"`. */
|
|
554
|
+
isoControl: "manual" | "automatic";
|
|
555
|
+
/** #516: present with `iso: "auto"`: how Auto ISO chose `settings.iso` and what limited it. */
|
|
556
|
+
autoIso?: SimulateCaptureAutoIsoReport;
|
|
433
557
|
meteredSceneLuminanceCdPerSquareMeter: number;
|
|
558
|
+
/**
|
|
559
|
+
* #515: deviation, in stops, of the exposure used from the CIPA DC-004-2020 standard exposure for the metered luminance L:
|
|
560
|
+
* log2(N²/t) − log2(0.65·L·S/10). Positive is darker than the standard exposure (under), negative brighter (over). In an
|
|
561
|
+
* automatic mode it is the compensation with the opposite sign plus any step or limit the resolver hit. Finite, or null when
|
|
562
|
+
* the metered luminance is 0 (a black scene has no standard exposure).
|
|
563
|
+
*/
|
|
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;
|
|
434
572
|
/** Full output of the existing exposure-mode resolver. */
|
|
435
573
|
resolution: unknown;
|
|
436
574
|
};
|
|
@@ -619,6 +757,9 @@ export interface IdealReferenceResult {
|
|
|
619
757
|
iso: number;
|
|
620
758
|
};
|
|
621
759
|
ev100: number;
|
|
760
|
+
/** #516: as in the capture result. */
|
|
761
|
+
isoControl: "manual" | "automatic";
|
|
762
|
+
autoIso?: SimulateCaptureAutoIsoReport;
|
|
622
763
|
};
|
|
623
764
|
scene: SimulateCaptureResult["scene"];
|
|
624
765
|
region?: {
|
|
@@ -699,7 +840,7 @@ export interface DisplayViewResult {
|
|
|
699
840
|
height: number;
|
|
700
841
|
};
|
|
701
842
|
};
|
|
702
|
-
/** The resolved exposure of the capture this view shows. */
|
|
843
|
+
/** The resolved exposure of the capture this view shows; `standardExposureDeviationStops` and `standardExposureIlluminanceLux` as in {@link SimulateCaptureResult}. */
|
|
703
844
|
exposure: {
|
|
704
845
|
mode: SimulateCaptureExposureMode;
|
|
705
846
|
settings: {
|
|
@@ -708,6 +849,12 @@ export interface DisplayViewResult {
|
|
|
708
849
|
iso: number;
|
|
709
850
|
};
|
|
710
851
|
ev100: number;
|
|
852
|
+
standardExposureDeviationStops: number | null;
|
|
853
|
+
/** As in the capture result. */
|
|
854
|
+
standardExposureIlluminanceLux: number | null;
|
|
855
|
+
/** #516: as in the capture result. */
|
|
856
|
+
isoControl: "manual" | "automatic";
|
|
857
|
+
autoIso?: SimulateCaptureAutoIsoReport;
|
|
711
858
|
};
|
|
712
859
|
scene: SimulateCaptureResult["scene"];
|
|
713
860
|
/**
|
|
@@ -809,6 +956,15 @@ export interface DisplayViewRunContext {
|
|
|
809
956
|
regimeId: string;
|
|
810
957
|
}>;
|
|
811
958
|
resolvedIso: number;
|
|
959
|
+
/** #516: how the ISO was chosen, reported in the view's `exposure`. */
|
|
960
|
+
isoReport: {
|
|
961
|
+
isoControl: "manual" | "automatic";
|
|
962
|
+
autoIso?: SimulateCaptureAutoIsoReport;
|
|
963
|
+
};
|
|
964
|
+
/** #515: the capture's `exposure.standardExposureDeviationStops`. */
|
|
965
|
+
standardExposureDeviationStops: number | null;
|
|
966
|
+
/** The capture's `exposure.standardExposureIlluminanceLux`. */
|
|
967
|
+
standardExposureIlluminanceLux: number | null;
|
|
812
968
|
sceneOption: unknown;
|
|
813
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";
|
|
814
970
|
shutterSeconds: number;
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { markInternalNativeTileReader, markInternalSceneRadianceEvaluator, registerInternalRadianceAccess } from "../core/internal-radiance-evaluator.js";
|
|
32
32
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
33
|
+
import { InvalidScientificResultError } from "../core/validation.js";
|
|
33
34
|
import { requireInputObject } from "../core/record-validation.js";
|
|
34
35
|
import { ENGINE_API_VERSION } from "../core/version.js";
|
|
35
36
|
import { GENERIC_TIER_ASSETS } from "../equipment/generic-tier-assets.js";
|
|
@@ -39,7 +40,8 @@ import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposu
|
|
|
39
40
|
import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
40
41
|
import { meterRelativeExposure } from "../exposure/metering.js";
|
|
41
42
|
import { createExposureMeterTargetFromMeteringResult, setExposureCompensationOnMeterTarget } from "../exposure/metering-target.js";
|
|
42
|
-
import { resolveManualExposureMode, resolveAperturePriorityExposureMode, resolveShutterPriorityExposureMode, resolveProgramAutoExposureMode } from "../exposure/exposure-mode-resolver.js";
|
|
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";
|
|
43
45
|
import { parseExposureProgramLineProfile } from "../exposure/program-line.js";
|
|
44
46
|
import { resolveCaptureGeometry } from "../output/capture-geometry.js";
|
|
45
47
|
import { estimateCie1931ColorMatchingFunctions } from "../color/color-matching-functions.js";
|
|
@@ -121,6 +123,36 @@ const GREY_CARD_REFLECTANCE = 0.18;
|
|
|
121
123
|
function sosReferenceLuminance(fNumber, shutterSeconds, iso) {
|
|
122
124
|
return SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS * fNumber ** 2 / (CAMERA_EQUATION_CONSTANT * shutterSeconds * iso);
|
|
123
125
|
}
|
|
126
|
+
/**
|
|
127
|
+
* #515: deviation, in stops, of the exposure the settings give from the CIPA DC-004-2020 standard exposure for the metered
|
|
128
|
+
* luminance L: Δ = log2(N²/t) − log2(0.65·L·S/10) = log2(L_ref(N, t, S) / L), with L_ref the luminance that the settings
|
|
129
|
+
* expose at H = 10/S (S = 10/H, §3.2; H = 0.65·L·t/N², §5.5 (4)). Positive is darker than the standard exposure (under).
|
|
130
|
+
* A scene with no metered luminance (L = 0, which manual mode still captures) has no standard exposure: null, never ±Infinity.
|
|
131
|
+
*/
|
|
132
|
+
function standardExposureDeviationStops(fNumber, shutterSeconds, iso, meteredLuminance) {
|
|
133
|
+
if (!(meteredLuminance > 0))
|
|
134
|
+
return null;
|
|
135
|
+
const deviation = Math.log2(sosReferenceLuminance(fNumber, shutterSeconds, iso) / meteredLuminance);
|
|
136
|
+
if (!Number.isFinite(deviation))
|
|
137
|
+
throw new InvalidScientificResultError("exposure.standardExposureDeviationStops is not finite (metered scene luminance " +
|
|
138
|
+
meteredLuminance + " cd/m²).");
|
|
139
|
+
return deviation;
|
|
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
|
+
}
|
|
124
156
|
/**
|
|
125
157
|
* Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
126
158
|
* output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
|
|
@@ -392,6 +424,92 @@ export function bilinearPhaseProfiles() {
|
|
|
392
424
|
kernels: CHANNELS.map((c) => ({ outputChannelId: c, contributions: kernels[px + "," + py][c] })), evidence: FACADE_EVIDENCE,
|
|
393
425
|
limitations: ["Bilinear interpolation of the RGGB mosaic; no edge-directed demosaic, false-colour suppression or calibration."] } }));
|
|
394
426
|
}
|
|
427
|
+
/**
|
|
428
|
+
* #516: the result's Auto ISO report, read from a resolved Auto ISO resolution (every mode's resolved Auto ISO branch carries
|
|
429
|
+
* `idealIsoBeforeConstraints`, `isoResolution` and a resolved `targetResidual`; aperture priority adds its fallback flag).
|
|
430
|
+
*/
|
|
431
|
+
function autoIsoReport(resolution, iso, shutterPolicy, limits, automatic) {
|
|
432
|
+
const r = resolution;
|
|
433
|
+
// Program reports its hold under slowestPreferredShutter (#504); aperture priority at the top level.
|
|
434
|
+
const program = r.slowestPreferredShutter;
|
|
435
|
+
const ideal = r.idealIsoBeforeConstraints, residual = r.targetResidual?.residualStops, clamped = r.isoResolution?.clamped;
|
|
436
|
+
if (typeof ideal !== "number" || !Number.isFinite(ideal) || typeof residual !== "number" || !Number.isFinite(residual) || r.targetResidual?.status !== "resolved" ||
|
|
437
|
+
(clamped !== false && clamped !== "minimum" && clamped !== "maximum"))
|
|
438
|
+
throw new InvalidScientificResultError("Auto ISO resolution did not report a finite ideal ISO and residual.");
|
|
439
|
+
// The resolvers' Auto ISO range: the body's Auto ISO bounds, else its ISO range.
|
|
440
|
+
return { idealIsoBeforeConstraints: ideal, autoIsoMinimum: iso.autoIsoMinimum ?? iso.minimum, autoIsoMaximum: iso.autoIsoMaximum ?? iso.maximum, clamped,
|
|
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;
|
|
512
|
+
}
|
|
395
513
|
function defaultProgramLine() {
|
|
396
514
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
397
515
|
// it first opens the aperture toward f/4, then lengthens the shutter. For more light it first closes toward
|
|
@@ -1186,11 +1304,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1186
1304
|
registerInternalRadianceAccess(provider.evaluateRadiance, { keyOfDirection: materialOfDirection, radianceOfKey: provider.materialRadiance, tabulated: true });
|
|
1187
1305
|
assume("model.scene-radiometry", "model", "lambertian-plane", "Radiance = reflectance × irradiance / π on one plane; illuminance from K_m = 683 lm/W and the engine's analytic CIE 1931 ȳ fit.");
|
|
1188
1306
|
// Metering (CIPA DC-004 standard output sensitivity and camera equation) and exposure-mode resolution
|
|
1189
|
-
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops"]);
|
|
1307
|
+
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops", "autoIso"]);
|
|
1190
1308
|
const mode = pick(exposureOptions, "mode", "exposure.mode", "manual", "Manual exposure.");
|
|
1191
1309
|
if (!["manual", "aperture-priority", "shutter-priority", "program"].includes(mode))
|
|
1192
1310
|
fail("Unknown exposure mode.");
|
|
1193
|
-
const
|
|
1311
|
+
const isoOption = pick(exposureOptions, "iso", "exposure.iso", DEFAULTS.iso, "ISO 100, the tiers' base ISO.");
|
|
1312
|
+
// #516: "auto" resolves the ISO with the mode's Auto ISO resolver; NaN marks the ISO as not yet known.
|
|
1313
|
+
const automaticIso = isoOption === "auto";
|
|
1314
|
+
if (typeof isoOption === "string" && !automaticIso)
|
|
1315
|
+
fail('exposure.iso must be a positive finite number or "auto".');
|
|
1316
|
+
const iso = automaticIso ? NaN : positive(isoOption, "exposure.iso");
|
|
1317
|
+
if (exposureOptions.autoIso !== undefined && !automaticIso)
|
|
1318
|
+
fail('exposure.autoIso applies only with exposure.iso "auto"; omit it.');
|
|
1194
1319
|
const compensation = exposureOptions.compensationStops === undefined ? DEFAULTS.compensationStops : exposureOptions.compensationStops;
|
|
1195
1320
|
if (typeof compensation !== "number" || !Number.isFinite(compensation) || Math.abs(compensation) > 10)
|
|
1196
1321
|
fail("exposure.compensationStops must be within ±10.");
|
|
@@ -1238,9 +1363,68 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1238
1363
|
fail("exposure.shutterSeconds is resolved automatically in " + mode + " mode; omit it.");
|
|
1239
1364
|
shutterSeconds = NaN;
|
|
1240
1365
|
}
|
|
1366
|
+
// #516: the resolvers' Auto ISO policies; their baseline is the lowest selectable ISO in the body's Auto ISO range.
|
|
1367
|
+
const autoIsoBaseline = { isoBaseline: "minimum-selectable", isoQuantizationPolicy: "nearest-log2-lower-on-tie" };
|
|
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;
|
|
1375
|
+
if (automaticIso) {
|
|
1376
|
+
if (capabilities.iso.autoIsoAvailability !== "supported")
|
|
1377
|
+
fail('exposure.iso "auto" needs a body with Auto ISO; this camera\'s Auto ISO is ' + capabilities.iso.autoIsoAvailability + ".");
|
|
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))) {
|
|
1396
|
+
if (autoIsoOptions.slowestPreferredShutterSeconds === undefined)
|
|
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
|
+
}
|
|
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.");
|
|
1414
|
+
if (afterMaximumIso !== "allow-slower-shutter" && afterMaximumIso !== "hold-preferred-shutter")
|
|
1415
|
+
fail('exposure.autoIso.afterMaximumIso must be "allow-slower-shutter" or "hold-preferred-shutter".');
|
|
1416
|
+
autoIsoShutterPolicy = { slowestPreferredShutterSeconds, afterMaximumIso };
|
|
1417
|
+
if (mode === "aperture-priority")
|
|
1418
|
+
autoIsoAperturePriority = autoIsoShutterPolicy;
|
|
1419
|
+
}
|
|
1420
|
+
const limitsPolicy = autoIsoLimits ? { autoIsoLimits: { ...autoIsoLimits } } : {};
|
|
1241
1421
|
if (mode === "manual") {
|
|
1242
1422
|
resolution = resolveManualExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
|
|
1243
|
-
isoControl: { kind: "manual", iso } });
|
|
1423
|
+
isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy, ...limitsPolicy } : { kind: "manual", iso } });
|
|
1424
|
+
}
|
|
1425
|
+
else if (autoIsoAperturePriority) {
|
|
1426
|
+
resolution = resolveAperturePriorityAutoIsoExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber,
|
|
1427
|
+
policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority, ...limitsPolicy } });
|
|
1244
1428
|
}
|
|
1245
1429
|
else if (mode === "aperture-priority") {
|
|
1246
1430
|
resolution = resolveAperturePriorityExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
|
|
@@ -1248,17 +1432,25 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1248
1432
|
}
|
|
1249
1433
|
else if (mode === "shutter-priority") {
|
|
1250
1434
|
resolution = resolveShutterPriorityExposureMode({ target, capabilities, referenceExposure: reference, manualShutterSeconds: shutterSeconds,
|
|
1251
|
-
isoControl: { kind: "
|
|
1435
|
+
isoControl: automaticIso ? { kind: "automatic", policy: { kind: "minimum-iso-aperture-first", apertureSelectionPolicy: "not-wider-than-target", ...autoIsoBaseline, ...limitsPolicy } } :
|
|
1436
|
+
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1252
1437
|
}
|
|
1253
1438
|
else {
|
|
1254
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.");
|
|
1255
|
-
resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(),
|
|
1440
|
+
resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(),
|
|
1441
|
+
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1442
|
+
{ kind: "manual", iso } });
|
|
1256
1443
|
}
|
|
1257
1444
|
if (resolution.status !== "resolved" || !resolution.resolvedSettings)
|
|
1258
1445
|
fail("Exposure could not be resolved (" + resolution.status + "); the scene may have no signal.");
|
|
1259
1446
|
fNumber = resolution.resolvedSettings.aperture ?? fNumber;
|
|
1260
1447
|
shutterSeconds = resolution.resolvedSettings.shutterSeconds ?? shutterSeconds;
|
|
1261
1448
|
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
1449
|
+
if (!Number.isFinite(resolvedIso) || resolvedIso <= 0)
|
|
1450
|
+
fail("Exposure could not be resolved: the resolved ISO is not a positive finite number.");
|
|
1451
|
+
const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution, capabilities.iso, autoIsoShutterPolicy, autoIsoLimits, automaticSlowestShutter) } : { isoControl: "manual" };
|
|
1452
|
+
const exposureDeviationStops = standardExposureDeviationStops(fNumber, shutterSeconds, resolvedIso, meteredLuminance);
|
|
1453
|
+
const exposureIlluminanceLux = standardExposureIlluminanceLux(fNumber, shutterSeconds, resolvedIso, meteredLuminance, illuminanceLux, callerScene?.kind !== "radiance-function");
|
|
1262
1454
|
const defocusDisc = defocusModel === undefined ? undefined :
|
|
1263
1455
|
resolveDefocusOption({ run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch, diffraction: diffractionRadiusOption !== undefined });
|
|
1264
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.
|
|
@@ -1699,7 +1891,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1699
1891
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1700
1892
|
// that only captures does not contain the display view.
|
|
1701
1893
|
if (displayView)
|
|
1702
|
-
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, 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 });
|
|
1703
1895
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1704
1896
|
if (idealReference) {
|
|
1705
1897
|
const referenceStarted = clockMs();
|
|
@@ -1772,7 +1964,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1772
1964
|
"Each pixel samples the scene at its centre, like the capture's default point aperture; use sensor.pixelAperture \"box\" for photosite-averaged pixels.");
|
|
1773
1965
|
return Object.freeze({ kind: "ideal-reference", label: "Ideal reference (no camera effects); not a capture.", facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
1774
1966
|
image: { width: outputWidth, height: outputHeight, format, samples, ...(png ? { png, colorEncoding: colorEncoding } : {}) },
|
|
1775
|
-
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds) },
|
|
1967
|
+
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport },
|
|
1776
1968
|
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
1777
1969
|
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
1778
1970
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|
|
@@ -2062,8 +2254,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2062
2254
|
...(region && regionNative ? { region: { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2063
2255
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2064
2256
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
2065
|
-
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds),
|
|
2066
|
-
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance,
|
|
2257
|
+
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
|
|
2258
|
+
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2259
|
+
standardExposureIlluminanceLux: exposureIlluminanceLux, resolution },
|
|
2067
2260
|
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
2068
2261
|
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
2069
2262
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|