@photivra/engine 2.12.0 → 2.14.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 +144 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -7
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +3 -0
- package/dist/camera/lens-projection.js +2 -1
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/capture/dng-import.js +1 -1
- package/dist/capture/dng-import.js.map +1 -1
- 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/capture/host-yield-cadence.js +1 -1
- package/dist/capture/host-yield-cadence.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.js +1 -1
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-raw.js +1 -1
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/packed-sensor-raw.js +3 -3
- package/dist/capture/packed-sensor-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +1 -1
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/color/capture-color.js +3 -3
- package/dist/color/capture-color.js.map +1 -1
- package/dist/color/chromatic-adaptation.js +2 -2
- package/dist/color/chromatic-adaptation.js.map +1 -1
- package/dist/color/color-matching-functions.js +1 -1
- package/dist/color/color-matching-functions.js.map +1 -1
- package/dist/color/colorimetry-math.js +2 -2
- package/dist/color/colorimetry-math.js.map +1 -1
- package/dist/color/illuminants.js +3 -3
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/rgb-color-spaces.js +1 -1
- package/dist/color/rgb-color-spaces.js.map +1 -1
- package/dist/color/sensor-color-development.js +1 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/core/record-validation.js +3 -3
- package/dist/core/record-validation.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/generic-prime-lenses.js +3 -3
- package/dist/equipment/generic-prime-lenses.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +23 -1
- package/dist/equipment/generic-tier-assets.js +58 -30
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.d.ts +6 -3
- package/dist/equipment/generic-tier-presets.js +26 -13
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.d.ts +120 -0
- package/dist/exposure/auto-iso-slowest-shutter.js +217 -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/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -1
- package/dist/exposure/flash.js.map +1 -1
- package/dist/facade/chart-material-layers.js +2 -2
- package/dist/facade/chart-material-layers.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +1 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +45 -8
- package/dist/facade/chart-scenes.js +101 -44
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/color-patches.d.ts +129 -0
- package/dist/facade/color-patches.js +209 -0
- package/dist/facade/color-patches.js.map +1 -0
- package/dist/facade/diffraction-provider.js +12 -13
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu.js +1 -1
- package/dist/facade/display-view-gpu.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/scene-radiance.js +2 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +139 -12
- package/dist/facade/simulate-capture.js +209 -36
- 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/chart-composite-gpu.js +2 -2
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.js +1 -1
- package/dist/gpu/display-view-composite-material-module.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/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/temporal-sum-gpu.js +23 -23
- package/dist/gpu/webgpu-runtime.js +1 -1
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/gpu/wgsl-chart-composite-kernel.js +318 -318
- package/dist/gpu/wgsl-chart-composite-kernel.js.map +1 -1
- package/dist/gpu/wgsl-chart-kernel.js +117 -117
- package/dist/gpu/wgsl-chart-kernel.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 +522 -488
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js +248 -248
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-kernels.js +203 -203
- package/dist/gpu/wgsl-kernels.js.map +1 -1
- package/dist/gpu/wgsl-sensor-kernel.js +302 -302
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6 -3
- package/dist/index.js.map +1 -1
- package/dist/motion/temporal-exposure-sum.js +1 -1
- package/dist/motion/temporal-exposure-sum.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.js +4 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/front-of-lens-filter.js +1 -1
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +10 -0
- package/dist/optics/irradiance-convolution.js +49 -14
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/output/native-capture-sdr.js +1 -1
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/png-encoder.js +2 -2
- package/dist/output/png-encoder.js.map +1 -1
- package/dist/output/print-jpeg.js +13 -10
- 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/dist/sensor/fixed-pattern-noise.js +1 -1
- package/dist/sensor/fixed-pattern-noise.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -1
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/docs/API_REFERENCE.md +49 -1
- package/docs/QUICK_START.md +104 -9
- package/docs/USAGE.md +241 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-tier-assets.md +41 -1
- package/docs/api/equipment-generic-tier-presets.md +11 -3
- package/docs/api/exports.json +172 -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-chart-scenes.md +36 -5
- package/docs/api/facade-color-patches.md +150 -0
- package/docs/api/facade-simulate-capture.md +140 -9
- package/docs/api/output-text-metadata.md +34 -0
- package/llms.txt +1 -0
- package/package.json +2 -2
|
@@ -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";
|
|
@@ -90,13 +91,14 @@ import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
|
90
91
|
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, resolveChartSizeOption, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W, planarProviderMaterialOfDirection } from "./scene-radiance.js";
|
|
91
92
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
92
93
|
export const SIMULATE_CAPTURE_FACADE_VERSION = "1.0.0";
|
|
94
|
+
export const SIMULATE_CAPTURE_METERING_MODES = /* @__PURE__ */ Object.freeze(["scene-average", "grey-card"]);
|
|
93
95
|
/** Default midpoint samples per axis of `sensor.pixelAperture: "box"`. */
|
|
94
96
|
export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
95
97
|
/**
|
|
96
98
|
* Options reported in `diagnostics.options` (2.1), in this order, when they ran: the ones that change how much work
|
|
97
99
|
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
98
100
|
*/
|
|
99
|
-
export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
101
|
+
export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
100
102
|
"lens.diffraction", "lens.defocus", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
101
103
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
102
104
|
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
@@ -105,10 +107,10 @@ export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling"
|
|
|
105
107
|
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
106
108
|
* whether or not it has a fast method yet; one without runs exact and reports `false`.
|
|
107
109
|
*/
|
|
108
|
-
export const SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
109
|
-
export const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
110
|
+
export const SIMULATE_CAPTURE_FAST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
111
|
+
export const FACADE_EVIDENCE = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({ sourceOrigin: "photivra",
|
|
110
112
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
111
|
-
const DEFAULTS = Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
113
|
+
const DEFAULTS = /* @__PURE__ */ Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
112
114
|
subjectDistanceM: 5, width: 64, height: 64, format: "srgb-8", toneCurve: "identity", highlightMode: "clip",
|
|
113
115
|
whiteBalance: "scene-illuminant", jpegQuantizationStep: 2, pixelPitchMicrometers: 6, spectralStepNanometers: 20,
|
|
114
116
|
temporalSampleCount: 1, noiseSeed: 1, illuminantKelvin: 6500 });
|
|
@@ -137,6 +139,33 @@ function standardExposureDeviationStops(fNumber, shutterSeconds, iso, meteredLum
|
|
|
137
139
|
meteredLuminance + " cd/m²).");
|
|
138
140
|
return deviation;
|
|
139
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* The illuminance at which the settings give the standard exposure, E·L_ref/L (= E·2^Δ). A reflective scene's radiance is
|
|
144
|
+
* reflectance × k(E)·S(λ)/π with k linear in E (calculateIlluminantIrradianceScale), so the metered L is proportional to E
|
|
145
|
+
* and re-metering at this illuminance gives Δ = 0 up to rounding. A radiance-function scene ignores E, and a black scene
|
|
146
|
+
* has no standard exposure: null.
|
|
147
|
+
*/
|
|
148
|
+
function standardExposureIlluminanceLux(fNumber, shutterSeconds, iso, meteredLuminance, illuminanceLux, radianceScalesWithIlluminance) {
|
|
149
|
+
if (!radianceScalesWithIlluminance || !(meteredLuminance > 0))
|
|
150
|
+
return null;
|
|
151
|
+
const lux = illuminanceLux * sosReferenceLuminance(fNumber, shutterSeconds, iso) / meteredLuminance;
|
|
152
|
+
if (!Number.isFinite(lux))
|
|
153
|
+
throw new InvalidScientificResultError("exposure.standardExposureIlluminanceLux is not finite (metered scene luminance " +
|
|
154
|
+
meteredLuminance + " cd/m²).");
|
|
155
|
+
return lux;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 2.14: deviation, in stops, of the settings from the standard exposure of an 18 % Lambertian grey card under illuminance E:
|
|
159
|
+
* Δ_grey = log2(L_ref(N, t, S) / (0.18·E/π)), computed as log2((π·L_ref/0.18) / E) so that at the default settings the
|
|
160
|
+
* numerator is SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX in the same operations, and the default request gives exactly 0.
|
|
161
|
+
* The grey card's luminance L = ρ·E/π is that of a Lambertian reflector (luminance = illuminance × reflectance / π).
|
|
162
|
+
*/
|
|
163
|
+
function greyCardDeviationStops(fNumber, shutterSeconds, iso, illuminanceLux) {
|
|
164
|
+
const deviation = Math.log2(Math.PI * sosReferenceLuminance(fNumber, shutterSeconds, iso) / GREY_CARD_REFLECTANCE / illuminanceLux);
|
|
165
|
+
if (!Number.isFinite(deviation))
|
|
166
|
+
throw new InvalidScientificResultError("exposure.greyCardDeviationStops is not finite (illuminance " + illuminanceLux + " lux).");
|
|
167
|
+
return deviation;
|
|
168
|
+
}
|
|
140
169
|
/**
|
|
141
170
|
* Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
142
171
|
* output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
|
|
@@ -268,7 +297,7 @@ export function apertureNodeOffsets(n) {
|
|
|
268
297
|
const AIRY_THIRD_ZERO_PER_LAMBDA_N = 10.173468135062722 / Math.PI;
|
|
269
298
|
/** Default temporal midpoints when the camera rotates (2.0 motion option). */
|
|
270
299
|
const ROTATING_TEMPORAL_SAMPLE_COUNT = 8;
|
|
271
|
-
const IDS = Object.freeze({ frame: "facade-frame", mode: "native-still", cfa: "photivra-generic-bayer", binding: "facade-native-cfa",
|
|
300
|
+
const IDS = /* @__PURE__ */ Object.freeze({ frame: "facade-frame", mode: "native-still", cfa: "photivra-generic-bayer", binding: "facade-native-cfa",
|
|
272
301
|
sceneState: "facade-scene-state", scene: "facade-scene", provider: "facade-provider", lights: "facade-lights", materials: "facade-materials",
|
|
273
302
|
capture: "facade-capture", spectral: "facade-colorimetric-eqe", sampling: "facade-sampling", stack: "facade-no-aa", stackAa: "facade-aa",
|
|
274
303
|
stationarity: "facade-stationarity", operatingState: "facade-operating-state", dark: "facade-dark", completeness: "facade-completeness",
|
|
@@ -280,8 +309,8 @@ export const CHANNELS = ["red", "green", "blue"];
|
|
|
280
309
|
* proportional to CIE XYZ (the Luther–Ives condition holds, up to quadrature error), and the camera→XYZ
|
|
281
310
|
* matrix is diagonal.
|
|
282
311
|
*/
|
|
283
|
-
const EQE_SCALE = Object.freeze({ red: 0.5, green: 0.5, blue: 0.25 });
|
|
284
|
-
const CMF_KEY = Object.freeze({ red: "x", green: "y", blue: "z" });
|
|
312
|
+
const EQE_SCALE = /* @__PURE__ */ Object.freeze({ red: 0.5, green: 0.5, blue: 0.25 });
|
|
313
|
+
const CMF_KEY = /* @__PURE__ */ Object.freeze({ red: "x", green: "y", blue: "z" });
|
|
285
314
|
export const CAMERA_TO_XYZ = Object.freeze([[EQE_SCALE.green / EQE_SCALE.red, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.green / EQE_SCALE.blue]].map((r) => Object.freeze(r)));
|
|
286
315
|
const XYZ_TO_CAMERA = [[EQE_SCALE.red / EQE_SCALE.green, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.blue / EQE_SCALE.green]];
|
|
287
316
|
/** Synthetic dark current per site at the operating temperature, e⁻/s. */
|
|
@@ -289,7 +318,7 @@ export const DARK_CURRENT_ELECTRONS_PER_SECOND = 1;
|
|
|
289
318
|
const OPERATING_TEMPERATURE_C = 20;
|
|
290
319
|
export function fail(message) { throw new InvalidConfigurationError(message); }
|
|
291
320
|
/** Where `output.metadata` goes in each file format (reported in the `output.metadata` assumption). */
|
|
292
|
-
const TEXT_METADATA_LAYOUT = Object.freeze({
|
|
321
|
+
const TEXT_METADATA_LAYOUT = /* @__PURE__ */ Object.freeze({
|
|
293
322
|
jpeg: "XMP packet in APP1 (dc:description, xmp:CreatorTool, dc:rights, exif:UserComment), plus a COM segment holding the comment",
|
|
294
323
|
png: "One tEXt (printable Latin-1) or iTXt (UTF-8) chunk per field, keywords Description, Software, Copyright, Comment, before the first IDAT",
|
|
295
324
|
dng: "ImageDescription (statement, then the description) and Copyright when printable ASCII; an XMP packet in tag 700 with every field"
|
|
@@ -374,6 +403,19 @@ function luminanceOf(radiance) {
|
|
|
374
403
|
}
|
|
375
404
|
return PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W * sum;
|
|
376
405
|
}
|
|
406
|
+
/**
|
|
407
|
+
* 2.14: the capabilities with the ISO grid cut to its whole stops (ISO 100·2^k, as the 2.0.0 tier bodies list them), for
|
|
408
|
+
* Auto ISO's default `isoSteps: "whole-stop"`. A whole-stop grid is returned unchanged, so 2.0.0 records resolve as before.
|
|
409
|
+
*/
|
|
410
|
+
function wholeStopIsoCapabilities(capabilities) {
|
|
411
|
+
const grid = capabilities.iso.settingGrid;
|
|
412
|
+
if (grid.kind !== "discrete-values")
|
|
413
|
+
return capabilities;
|
|
414
|
+
const whole = grid.values.filter((iso) => Number.isInteger(Math.log2(iso / 100)));
|
|
415
|
+
if (whole.length === grid.values.length || whole.length === 0)
|
|
416
|
+
return capabilities;
|
|
417
|
+
return { ...capabilities, iso: { ...capabilities.iso, settingGrid: { kind: "discrete-values", values: Object.freeze(whole) } } };
|
|
418
|
+
}
|
|
377
419
|
/** The white balance measured from a perfect white diffuser under the scene illuminant ("scene-illuminant"). */
|
|
378
420
|
export function sceneIlluminantWhiteBalance(illuminant) {
|
|
379
421
|
const xyz = illuminantXyz(illuminant), camera = XYZ_TO_CAMERA.map((row) => row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]);
|
|
@@ -412,16 +454,87 @@ export function bilinearPhaseProfiles() {
|
|
|
412
454
|
* #516: the result's Auto ISO report, read from a resolved Auto ISO resolution (every mode's resolved Auto ISO branch carries
|
|
413
455
|
* `idealIsoBeforeConstraints`, `isoResolution` and a resolved `targetResidual`; aperture priority adds its fallback flag).
|
|
414
456
|
*/
|
|
415
|
-
function autoIsoReport(resolution, iso,
|
|
457
|
+
function autoIsoReport(resolution, iso, shutterPolicy, limits, automatic, isoSteps) {
|
|
416
458
|
const r = resolution;
|
|
459
|
+
// Program reports its hold under slowestPreferredShutter (#504); aperture priority at the top level.
|
|
460
|
+
const program = r.slowestPreferredShutter;
|
|
417
461
|
const ideal = r.idealIsoBeforeConstraints, residual = r.targetResidual?.residualStops, clamped = r.isoResolution?.clamped;
|
|
418
462
|
if (typeof ideal !== "number" || !Number.isFinite(ideal) || typeof residual !== "number" || !Number.isFinite(residual) || r.targetResidual?.status !== "resolved" ||
|
|
419
463
|
(clamped !== false && clamped !== "minimum" && clamped !== "maximum"))
|
|
420
464
|
throw new InvalidScientificResultError("Auto ISO resolution did not report a finite ideal ISO and residual.");
|
|
421
465
|
// The resolvers' Auto ISO range: the body's Auto ISO bounds, else its ISO range.
|
|
422
466
|
return { idealIsoBeforeConstraints: ideal, autoIsoMinimum: iso.autoIsoMinimum ?? iso.minimum, autoIsoMaximum: iso.autoIsoMaximum ?? iso.maximum, clamped,
|
|
423
|
-
limitingConstraint: r.targetResidual.limitingConstraint, residualStops: residual,
|
|
424
|
-
...(
|
|
467
|
+
...(limits ? { limits: { ...limits } } : {}), limitingConstraint: r.targetResidual.limitingConstraint, residualStops: residual,
|
|
468
|
+
...(shutterPolicy ? { ...shutterPolicy, afterMaximumIsoFallbackUsed: (program ? program.afterMaximumIsoFallbackUsed : r.afterMaximumIsoFallbackUsed) === true } : {}),
|
|
469
|
+
...(program ? { slowestPreferredShutterApplied: program.applied === true } : {}),
|
|
470
|
+
...(automatic ? { automaticSlowestShutter: automatic } : {}), ...(isoSteps ? { isoSteps } : {}) };
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* #505: `exposure.autoIso.limits`, the photographer's Auto ISO range. Both limits are selectable ISOs inside the body's
|
|
474
|
+
* Auto ISO range, `minimum` at most `maximum`; anything else fails (the resolvers check the same, but an option defect
|
|
475
|
+
* is a configuration error here). `minimum` defaults to the body's Auto ISO minimum.
|
|
476
|
+
*/
|
|
477
|
+
function parseAutoIsoLimitsOption(value, iso) {
|
|
478
|
+
const record = allowKeys(value, "exposure.autoIso.limits", ["maximum", "minimum"]);
|
|
479
|
+
if (record.maximum === undefined)
|
|
480
|
+
fail("exposure.autoIso.limits.maximum is required: the highest ISO Auto ISO may choose.");
|
|
481
|
+
const bodyMinimum = iso.autoIsoMinimum ?? iso.minimum, bodyMaximum = iso.autoIsoMaximum ?? iso.maximum;
|
|
482
|
+
const limit = (field) => {
|
|
483
|
+
const v = positive(record[field], "exposure.autoIso.limits." + field);
|
|
484
|
+
if (v < bodyMinimum || v > bodyMaximum)
|
|
485
|
+
fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be within the body's Auto ISO range, ISO " + bodyMinimum + " to " + bodyMaximum + ".");
|
|
486
|
+
if (iso.settingGrid.kind === "discrete-values" && !iso.settingGrid.values.includes(v))
|
|
487
|
+
fail("exposure.autoIso.limits." + field + " (ISO " + v + ") must be a selectable ISO of the body: " + iso.settingGrid.values.join(", ") + ".");
|
|
488
|
+
return v;
|
|
489
|
+
};
|
|
490
|
+
const maximum = limit("maximum"), minimum = record.minimum === undefined ? bodyMinimum : limit("minimum");
|
|
491
|
+
if (minimum > maximum)
|
|
492
|
+
fail("exposure.autoIso.limits.minimum (ISO " + minimum + ") must not exceed exposure.autoIso.limits.maximum (ISO " + maximum + ").");
|
|
493
|
+
return { minimum, maximum };
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* #504: `exposure.autoIso.slowestPreferredShutterSeconds: "auto"`: the slowest preferred shutter from the camera-shake model
|
|
497
|
+
* (`estimateAutoIsoSlowestShutter`) for this lens, focus and pixel pitch, within the body's shutter range. Option defects are
|
|
498
|
+
* configuration errors; the defaults are recorded as `default` assumptions and the derivation as a `model` assumption.
|
|
499
|
+
*/
|
|
500
|
+
function resolveAutomaticSlowestShutterOption(value, context) {
|
|
501
|
+
const path = "exposure.autoIso.automaticSlowestShutter";
|
|
502
|
+
const record = allowKeys(value, path, ["fasterByStops", "blurBudgetPixels", "angularVelocityRadPerSec", "stabilizationStopsEquivalent"]);
|
|
503
|
+
const fasterByStops = context.pick(record, "fasterByStops", path + ".fasterByStops", 0, "No bias: the derived slowest shutter is used as is.");
|
|
504
|
+
if (typeof fasterByStops !== "number" || !Number.isInteger(fasterByStops) || Math.abs(fasterByStops) > 2)
|
|
505
|
+
fail(path + ".fasterByStops must be a whole number of stops from -2 to +2.");
|
|
506
|
+
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");
|
|
507
|
+
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 " +
|
|
508
|
+
"holding a 100 g mobile handset with two hands. A labelled generic default, not a measurement of this photographer or camera.");
|
|
509
|
+
const angular = allowKeys(angularOption, path + ".angularVelocityRadPerSec", ["yaw", "pitch"]);
|
|
510
|
+
for (const axis of ["yaw", "pitch"]) {
|
|
511
|
+
const v = angular[axis];
|
|
512
|
+
if (typeof v !== "number" || !Number.isFinite(v) || Math.abs(v) > 100)
|
|
513
|
+
fail(path + ".angularVelocityRadPerSec." + axis + " must be a finite number within ±100 rad/s.");
|
|
514
|
+
}
|
|
515
|
+
const angularVelocityRadPerSec = { yaw: angular.yaw, pitch: angular.pitch };
|
|
516
|
+
if (angularVelocityRadPerSec.yaw === 0 && angularVelocityRadPerSec.pitch === 0)
|
|
517
|
+
fail(path + ".angularVelocityRadPerSec must be non-zero on at least one axis: without shake no slowest shutter follows.");
|
|
518
|
+
const stabilizationStopsEquivalent = context.pick(record, "stabilizationStopsEquivalent", path + ".stabilizationStopsEquivalent", 0, "No stabilization: the generic tiers declare none.");
|
|
519
|
+
if (typeof stabilizationStopsEquivalent !== "number" || !Number.isFinite(stabilizationStopsEquivalent) || stabilizationStopsEquivalent < 0 || stabilizationStopsEquivalent > 10)
|
|
520
|
+
fail(path + ".stabilizationStopsEquivalent must be a number of stops from 0 to 10.");
|
|
521
|
+
let estimate;
|
|
522
|
+
try {
|
|
523
|
+
estimate = estimateAutoIsoSlowestShutter({ focalLengthMm: context.focalLengthMm, focusDistanceM: context.focusDistanceM, angularVelocityRadPerSec,
|
|
524
|
+
stabilizationStopsEquivalent, blurBudget: { kind: "pixels", pixels: blurBudgetPixels, pixelPitchMicrometers: context.pitch }, fasterByStops,
|
|
525
|
+
shutterCapability: context.shutter }).value;
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
fail(path + ": " + (error instanceof Error ? error.message : String(error)));
|
|
529
|
+
}
|
|
530
|
+
const report = { derivedShutterSeconds: estimate.derivedShutterSeconds, fasterByStops,
|
|
531
|
+
biasedShutterSeconds: estimate.biasedShutterSeconds, shutterSeconds: estimate.shutterSeconds, blurBudgetPixels, blurAtShutterPixels: estimate.blurAtShutterPixels,
|
|
532
|
+
angularVelocityRadPerSec, stabilizationStopsEquivalent };
|
|
533
|
+
context.assume("exposure.autoIso.automaticSlowestShutter", "model", { ...report, angularVelocityRadPerSec: { ...angularVelocityRadPerSec },
|
|
534
|
+
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 " +
|
|
535
|
+
"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 " +
|
|
536
|
+
"reciprocal rule is a rule of thumb and is not used.");
|
|
537
|
+
return report;
|
|
425
538
|
}
|
|
426
539
|
function defaultProgramLine() {
|
|
427
540
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
@@ -1217,7 +1330,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1217
1330
|
registerInternalRadianceAccess(provider.evaluateRadiance, { keyOfDirection: materialOfDirection, radianceOfKey: provider.materialRadiance, tabulated: true });
|
|
1218
1331
|
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.");
|
|
1219
1332
|
// Metering (CIPA DC-004 standard output sensitivity and camera equation) and exposure-mode resolution
|
|
1220
|
-
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops", "autoIso"]);
|
|
1333
|
+
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops", "autoIso", "metering"]);
|
|
1334
|
+
// 2.14: no default assumption when omitted, so 2.13 requests keep their assumptions; the result reports `exposure.metering`.
|
|
1335
|
+
const metering = exposureOptions.metering === undefined ? "scene-average" : exposureOptions.metering;
|
|
1336
|
+
if (!SIMULATE_CAPTURE_METERING_MODES.includes(metering))
|
|
1337
|
+
fail('exposure.metering must be "scene-average" or "grey-card".');
|
|
1221
1338
|
const mode = pick(exposureOptions, "mode", "exposure.mode", "manual", "Manual exposure.");
|
|
1222
1339
|
if (!["manual", "aperture-priority", "shutter-priority", "program"].includes(mode))
|
|
1223
1340
|
fail("Unknown exposure mode.");
|
|
@@ -1227,8 +1344,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1227
1344
|
if (typeof isoOption === "string" && !automaticIso)
|
|
1228
1345
|
fail('exposure.iso must be a positive finite number or "auto".');
|
|
1229
1346
|
const iso = automaticIso ? NaN : positive(isoOption, "exposure.iso");
|
|
1230
|
-
if (exposureOptions.autoIso !== undefined && !
|
|
1231
|
-
fail('exposure.autoIso applies only
|
|
1347
|
+
if (exposureOptions.autoIso !== undefined && !automaticIso)
|
|
1348
|
+
fail('exposure.autoIso applies only with exposure.iso "auto"; omit it.');
|
|
1232
1349
|
const compensation = exposureOptions.compensationStops === undefined ? DEFAULTS.compensationStops : exposureOptions.compensationStops;
|
|
1233
1350
|
if (typeof compensation !== "number" || !Number.isFinite(compensation) || Math.abs(compensation) > 10)
|
|
1234
1351
|
fail("exposure.compensationStops must be within ±10.");
|
|
@@ -1249,6 +1366,15 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1249
1366
|
}
|
|
1250
1367
|
const meteredLuminance = luminanceSum / (zones * zones);
|
|
1251
1368
|
assume("exposure.metering", "model", { policy: meterProfile.policy.kind, zones: zones * zones }, "The body's " + meterProfile.policy.kind + " meter reads scene luminance on a 16 × 16 zone grid, before the lens (no lens transmission, vignetting or flare).");
|
|
1369
|
+
if (metering === "grey-card") {
|
|
1370
|
+
// 2.14: incident metering. Every zone reads the 18 % Lambertian grey card under the request's illuminance, L = 0.18·E/π.
|
|
1371
|
+
if (callerScene?.kind === "radiance-function")
|
|
1372
|
+
fail('exposure.metering "grey-card" needs a scene lit by the illuminance; a radiance-function scene ignores illuminanceLux.');
|
|
1373
|
+
const cardLuminance = GREY_CARD_REFLECTANCE * illuminanceLux / Math.PI;
|
|
1374
|
+
for (const sample of zoneSamples)
|
|
1375
|
+
sample.relativeLinearSignal = meterProfile.target.targetRelativeSignal * cardLuminance / referenceLuminance;
|
|
1376
|
+
assume("exposure.meteringSource", "model", { metering, greyCardReflectance: GREY_CARD_REFLECTANCE, luminanceCdPerSquareMeter: cardLuminance }, "Grey-card (incident) metering: every meter zone reads an 18 % Lambertian grey card under the scene illuminance, L = 0.18·E/π, instead of the scene.");
|
|
1377
|
+
}
|
|
1252
1378
|
const meterResult = meterRelativeExposure({ profile: meterProfile, sampleSet: { measurementId: "facade-meter", sceneStateId: IDS.sceneState,
|
|
1253
1379
|
inputDomain: "relative-pre-exposure-linear-signal", captureRegion: "oriented-active-capture", captureGeometry: resolvedGeometry,
|
|
1254
1380
|
processingState: { exposureSettingsApplied: false, whiteBalanceApplied: false, toneMappingApplied: false, displayGammaApplied: false, sharpeningApplied: false },
|
|
@@ -1279,45 +1405,89 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1279
1405
|
// #516: the resolvers' Auto ISO policies; their baseline is the lowest selectable ISO in the body's Auto ISO range.
|
|
1280
1406
|
const autoIsoBaseline = { isoBaseline: "minimum-selectable", isoQuantizationPolicy: "nearest-log2-lower-on-tie" };
|
|
1281
1407
|
let autoIsoAperturePriority;
|
|
1408
|
+
// #505: the photographer's Auto ISO limits, validated against the body's Auto ISO range; absent unless given.
|
|
1409
|
+
let autoIsoOptions = {};
|
|
1410
|
+
let autoIsoLimits;
|
|
1411
|
+
// #504: the slowest preferred shutter (aperture priority; optional in program) and, with "auto", how it was derived.
|
|
1412
|
+
let autoIsoShutterPolicy;
|
|
1413
|
+
let automaticSlowestShutter;
|
|
1414
|
+
// 2.14: the capabilities the exposure-mode resolvers see; with Auto ISO in whole stops, the ISO grid's whole stops only.
|
|
1415
|
+
let resolverCapabilities = capabilities, autoIsoIsoSteps = "whole-stop";
|
|
1282
1416
|
if (automaticIso) {
|
|
1283
1417
|
if (capabilities.iso.autoIsoAvailability !== "supported")
|
|
1284
1418
|
fail('exposure.iso "auto" needs a body with Auto ISO; this camera\'s Auto ISO is ' + capabilities.iso.autoIsoAvailability + ".");
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1419
|
+
autoIsoOptions = allowKeys(exposureOptions.autoIso, "exposure.autoIso", ["slowestPreferredShutterSeconds", "afterMaximumIso", "limits", "automaticSlowestShutter", "isoSteps"]);
|
|
1420
|
+
// 2.14: Auto ISO steps in whole stops unless asked for thirds, so its choices are those of 2.13 on the third-stop bodies.
|
|
1421
|
+
if (autoIsoOptions.isoSteps !== undefined && autoIsoOptions.isoSteps !== "whole-stop" && autoIsoOptions.isoSteps !== "third-stop")
|
|
1422
|
+
fail('exposure.autoIso.isoSteps must be "whole-stop" or "third-stop".');
|
|
1423
|
+
if (autoIsoOptions.isoSteps === "third-stop")
|
|
1424
|
+
autoIsoIsoSteps = "third-stop";
|
|
1425
|
+
if (autoIsoIsoSteps === "whole-stop")
|
|
1426
|
+
resolverCapabilities = wholeStopIsoCapabilities(capabilities);
|
|
1427
|
+
if (mode !== "aperture-priority" && mode !== "program")
|
|
1428
|
+
for (const key of ["slowestPreferredShutterSeconds", "afterMaximumIso", "automaticSlowestShutter"])
|
|
1429
|
+
if (autoIsoOptions[key] !== undefined)
|
|
1430
|
+
fail("exposure.autoIso." + key + ' applies only to exposure.mode "aperture-priority" or "program"; omit it.');
|
|
1431
|
+
if (autoIsoOptions.limits !== undefined)
|
|
1432
|
+
autoIsoLimits = parseAutoIsoLimitsOption(autoIsoOptions.limits, resolverCapabilities.iso);
|
|
1433
|
+
assume("exposure.autoIso", "model", { minimum: capabilities.iso.autoIsoMinimum ?? capabilities.iso.minimum, maximum: capabilities.iso.autoIsoMaximum ?? capabilities.iso.maximum,
|
|
1434
|
+
...(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 " +
|
|
1435
|
+
"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." +
|
|
1436
|
+
(autoIsoLimits ? " The photographer's limits (exposure.autoIso.limits, #505) narrow that range to ISO " + autoIsoLimits.minimum + " to " + autoIsoLimits.maximum + "." : ""));
|
|
1437
|
+
}
|
|
1438
|
+
if (automaticIso && mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds === undefined) {
|
|
1439
|
+
for (const key of ["afterMaximumIso", "automaticSlowestShutter"])
|
|
1440
|
+
if (autoIsoOptions[key] !== undefined)
|
|
1441
|
+
fail("exposure.autoIso." + key + ' in program needs exposure.autoIso.slowestPreferredShutterSeconds; omit it or set the slowest shutter.');
|
|
1442
|
+
}
|
|
1443
|
+
if (automaticIso && (mode === "aperture-priority" || (mode === "program" && autoIsoOptions.slowestPreferredShutterSeconds !== undefined))) {
|
|
1290
1444
|
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
|
-
|
|
1445
|
+
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".');
|
|
1446
|
+
if (autoIsoOptions.automaticSlowestShutter !== undefined && autoIsoOptions.slowestPreferredShutterSeconds !== "auto")
|
|
1447
|
+
fail('exposure.autoIso.automaticSlowestShutter applies only with exposure.autoIso.slowestPreferredShutterSeconds "auto"; omit it.');
|
|
1448
|
+
let slowestPreferredShutterSeconds;
|
|
1449
|
+
if (autoIsoOptions.slowestPreferredShutterSeconds === "auto") {
|
|
1450
|
+
automaticSlowestShutter = resolveAutomaticSlowestShutterOption(autoIsoOptions.automaticSlowestShutter, { focalLengthMm, focusDistanceM, pitch,
|
|
1451
|
+
shutter: capabilities.shutter, assume, pick });
|
|
1452
|
+
slowestPreferredShutterSeconds = automaticSlowestShutter.shutterSeconds;
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
if (typeof autoIsoOptions.slowestPreferredShutterSeconds === "string")
|
|
1456
|
+
fail('exposure.autoIso.slowestPreferredShutterSeconds must be a positive number of seconds or "auto".');
|
|
1457
|
+
slowestPreferredShutterSeconds = positive(autoIsoOptions.slowestPreferredShutterSeconds, "exposure.autoIso.slowestPreferredShutterSeconds");
|
|
1458
|
+
if (slowestPreferredShutterSeconds < capabilities.shutter.minimumSeconds || slowestPreferredShutterSeconds > capabilities.shutter.maximumSeconds)
|
|
1459
|
+
fail("exposure.autoIso.slowestPreferredShutterSeconds must be within the body's shutter range, " + capabilities.shutter.minimumSeconds + " s to " + capabilities.shutter.maximumSeconds + " s.");
|
|
1460
|
+
}
|
|
1295
1461
|
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
1462
|
if (afterMaximumIso !== "allow-slower-shutter" && afterMaximumIso !== "hold-preferred-shutter")
|
|
1297
1463
|
fail('exposure.autoIso.afterMaximumIso must be "allow-slower-shutter" or "hold-preferred-shutter".');
|
|
1298
|
-
|
|
1464
|
+
autoIsoShutterPolicy = { slowestPreferredShutterSeconds, afterMaximumIso };
|
|
1465
|
+
if (mode === "aperture-priority")
|
|
1466
|
+
autoIsoAperturePriority = autoIsoShutterPolicy;
|
|
1299
1467
|
}
|
|
1468
|
+
const limitsPolicy = autoIsoLimits ? { autoIsoLimits: { ...autoIsoLimits } } : {};
|
|
1300
1469
|
if (mode === "manual") {
|
|
1301
|
-
resolution = resolveManualExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
|
|
1302
|
-
isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy } : { kind: "manual", iso } });
|
|
1470
|
+
resolution = resolveManualExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
|
|
1471
|
+
isoControl: automaticIso ? { kind: "automatic", quantizationPolicy: autoIsoBaseline.isoQuantizationPolicy, ...limitsPolicy } : { kind: "manual", iso } });
|
|
1303
1472
|
}
|
|
1304
1473
|
else if (autoIsoAperturePriority) {
|
|
1305
|
-
resolution = resolveAperturePriorityAutoIsoExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber,
|
|
1306
|
-
policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority } });
|
|
1474
|
+
resolution = resolveAperturePriorityAutoIsoExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, manualAperture: fNumber,
|
|
1475
|
+
policy: { kind: "minimum-iso-until-slowest-preferred-shutter", ...autoIsoBaseline, shutterSelectionPolicy: "not-longer-than-target", ...autoIsoAperturePriority, ...limitsPolicy } });
|
|
1307
1476
|
}
|
|
1308
1477
|
else if (mode === "aperture-priority") {
|
|
1309
|
-
resolution = resolveAperturePriorityExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
|
|
1478
|
+
resolution = resolveAperturePriorityExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
|
|
1310
1479
|
shutterQuantizationPolicy: "nearest-log2-shorter-on-tie" });
|
|
1311
1480
|
}
|
|
1312
1481
|
else if (mode === "shutter-priority") {
|
|
1313
|
-
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 } } :
|
|
1482
|
+
resolution = resolveShutterPriorityExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, manualShutterSeconds: shutterSeconds,
|
|
1483
|
+
isoControl: automaticIso ? { kind: "automatic", policy: { kind: "minimum-iso-aperture-first", apertureSelectionPolicy: "not-wider-than-target", ...autoIsoBaseline, ...limitsPolicy } } :
|
|
1315
1484
|
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1316
1485
|
}
|
|
1317
1486
|
else {
|
|
1318
1487
|
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
|
-
resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(),
|
|
1320
|
-
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline
|
|
1488
|
+
resolution = resolveProgramAutoExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, programLine: defaultProgramLine(),
|
|
1489
|
+
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1490
|
+
{ kind: "manual", iso } });
|
|
1321
1491
|
}
|
|
1322
1492
|
if (resolution.status !== "resolved" || !resolution.resolvedSettings)
|
|
1323
1493
|
fail("Exposure could not be resolved (" + resolution.status + "); the scene may have no signal.");
|
|
@@ -1326,8 +1496,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1326
1496
|
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
1327
1497
|
if (!Number.isFinite(resolvedIso) || resolvedIso <= 0)
|
|
1328
1498
|
fail("Exposure could not be resolved: the resolved ISO is not a positive finite number.");
|
|
1329
|
-
const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution,
|
|
1499
|
+
const isoReport = automaticIso ? { isoControl: "automatic", autoIso: autoIsoReport(resolution, resolverCapabilities.iso, autoIsoShutterPolicy, autoIsoLimits, automaticSlowestShutter, autoIsoOptions.isoSteps === undefined ? undefined : autoIsoIsoSteps) } : { isoControl: "manual" };
|
|
1330
1500
|
const exposureDeviationStops = standardExposureDeviationStops(fNumber, shutterSeconds, resolvedIso, meteredLuminance);
|
|
1501
|
+
const exposureIlluminanceLux = standardExposureIlluminanceLux(fNumber, shutterSeconds, resolvedIso, meteredLuminance, illuminanceLux, callerScene?.kind !== "radiance-function");
|
|
1502
|
+
const exposureGreyCardDeviationStops = callerScene?.kind === "radiance-function" ? null : greyCardDeviationStops(fNumber, shutterSeconds, resolvedIso, illuminanceLux);
|
|
1331
1503
|
const defocusDisc = defocusModel === undefined ? undefined :
|
|
1332
1504
|
resolveDefocusOption({ run, assume, focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitch, diffraction: diffractionRadiusOption !== undefined });
|
|
1333
1505
|
// #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 +1940,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1768
1940
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1769
1941
|
// that only captures does not contain the display view.
|
|
1770
1942
|
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 });
|
|
1943
|
+
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, greyCardDeviationStops: exposureGreyCardDeviationStops, metering, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1772
1944
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1773
1945
|
if (idealReference) {
|
|
1774
1946
|
const referenceStarted = clockMs();
|
|
@@ -2132,7 +2304,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2132
2304
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2133
2305
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
2134
2306
|
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
|
|
2135
|
-
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2307
|
+
metering, meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2308
|
+
standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, resolution },
|
|
2136
2309
|
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
2137
2310
|
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
2138
2311
|
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|