@photivra/engine 2.19.0 → 2.20.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 +112 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -10
- package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
- package/dist/capture/dng-lens-correction-opcodes.js +119 -0
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
- package/dist/capture/native-environment-lean.js +1 -5
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +6 -0
- package/dist/capture/native-raw-dng.js +16 -2
- package/dist/capture/native-raw-dng.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +3 -5
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +2 -0
- package/dist/core/shared-helpers.js +4 -0
- package/dist/core/shared-helpers.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-lens-catalogue.d.ts +8 -2
- package/dist/equipment/generic-lens-catalogue.js +31 -2
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.js +2 -3
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.js +2 -3
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +7 -4
- package/dist/facade/display-view-gpu.js +4 -4
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +1 -1
- package/dist/facade/display-view-lens-signal.js +5 -3
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.d.ts +3 -1
- package/dist/facade/display-view.js +1 -1
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +50 -0
- package/dist/facade/fast-methods.js.map +1 -0
- package/dist/facade/fast-resolution.d.ts +12 -0
- package/dist/facade/fast-resolution.js +18 -0
- package/dist/facade/fast-resolution.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +3 -2
- package/dist/facade/generic-lens-capture.js +3 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -11
- package/dist/facade/simulate-capture.js +144 -75
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +3 -2
- package/dist/facade/simulate-display-view.js +22 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/aperture-diameter.d.ts +47 -0
- package/dist/optics/aperture-diameter.js +50 -0
- package/dist/optics/aperture-diameter.js.map +1 -0
- package/dist/optics/detail-limit.d.ts +182 -0
- package/dist/optics/detail-limit.js +336 -0
- package/dist/optics/detail-limit.js.map +1 -0
- package/dist/optics/generic-lens-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +25 -2
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/crop-to-match-focal-length.d.ts +62 -0
- package/dist/output/crop-to-match-focal-length.js +63 -0
- package/dist/output/crop-to-match-focal-length.js.map +1 -0
- package/dist/output/developed-lens-corrections.js +2 -2
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +3 -1
- package/dist/output/generic-lens-corrections.js +8 -4
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +12 -2
- package/dist/output/lens-corrections.js +22 -7
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/print-contrast-reference.js +1 -1
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-size-blur-noise.d.ts +110 -0
- package/dist/output/print-size-blur-noise.js +138 -0
- package/dist/output/print-size-blur-noise.js.map +1 -0
- package/dist/sensor/eqe-electron-rate.js +1 -7
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +2 -0
- package/dist/sensor/exposure-window.js +2 -2
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/readout-timing.js +2 -23
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.js +1 -6
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +1 -7
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sensor-characteristics.d.ts +137 -0
- package/dist/sensor/sensor-characteristics.js +115 -0
- package/dist/sensor/sensor-characteristics.js.map +1 -0
- package/dist/sensor/spatial-sample-reduction.js +1 -5
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +1 -6
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +3 -0
- package/dist/sensor/temporal-photo-signal.js +5 -2
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/stabilization/system.d.ts +9 -0
- package/dist/stabilization/system.js +5 -6
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +76 -2
- package/docs/QUICK_START.md +88 -6
- package/docs/USAGE.md +65 -2
- package/docs/api/capture-native-raw-dng.md +6 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +7 -2
- package/docs/api/exports.json +152 -2
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-fast-methods.md +91 -0
- package/docs/api/facade-simulate-capture.md +36 -11
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-generic-lens-corrections.md +3 -1
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +4 -0
- package/package.json +1 -1
|
@@ -36,6 +36,8 @@ import { ENGINE_API_VERSION } from "../core/version.js";
|
|
|
36
36
|
import { GENERIC_TIER_ASSETS } from "../equipment/generic-tier-assets.js";
|
|
37
37
|
import { parseCallerCameraPreset } from "./camera-preset.js";
|
|
38
38
|
import { createHostYield } from "./host-yield.js";
|
|
39
|
+
import { fastFormatDefault, resolveFast } from "./fast-resolution.js";
|
|
40
|
+
import { SCENE_TO_SENSOR_BRIDGE_SCOPE } from "../optics/scene-to-sensor-irradiance.js";
|
|
39
41
|
import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposure-capabilities.js";
|
|
40
42
|
import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
41
43
|
import { meterRelativeExposure } from "../exposure/metering.js";
|
|
@@ -91,7 +93,8 @@ import { multiplyMatrices } from "../color/colorimetry-math.js";
|
|
|
91
93
|
import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
92
94
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
93
95
|
import { correctDiffractionInPlace, defaultDiffractionCorrectionRadiusPixels, designDiffractionCorrectionFilter, parseDiffractionCorrectionSettings, parseUnsharpMaskSettings, renderSdrWithUnsharpMaskInPlace, DEVELOPMENT_DETAIL_MODEL_VERSION } from "../processing/development-detail.js";
|
|
94
|
-
import {
|
|
96
|
+
import { resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
97
|
+
import { characterizeReadout, parseSensorCharacteristics, sensorReport, SENSOR_CHARACTERISTIC_KEYS } from "../sensor/sensor-characteristics.js";
|
|
95
98
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
96
99
|
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
97
100
|
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, resolveChartSizeOption, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W, planarProviderMaterialOfDirection } from "./scene-radiance.js";
|
|
@@ -118,10 +121,12 @@ export const FACADE_EVIDENCE = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Ob
|
|
|
118
121
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
119
122
|
const DEFAULTS = /* @__PURE__ */ Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
120
123
|
subjectDistanceM: 5, width: 64, height: 64, format: "srgb-8", toneCurve: "identity", highlightMode: "clip",
|
|
121
|
-
whiteBalance: "scene-illuminant", jpegQuantizationStep:
|
|
124
|
+
whiteBalance: "scene-illuminant", jpegQuantizationStep: 1, pixelPitchMicrometers: 6, spectralStepNanometers: 20,
|
|
122
125
|
temporalSampleCount: 1, noiseSeed: 1, illuminantKelvin: 6500 });
|
|
123
126
|
/** Standard output sensitivity S = 10 / H (CIPA DC-004-2020 §3.2): the reference exposure in lx·s is 10 / S. */
|
|
124
127
|
const SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS = 10;
|
|
128
|
+
/** #565: the one-tap kernel that makes the image-space provider read each query at its lattice point. */
|
|
129
|
+
const UNIT_SITE_KERNEL = /* @__PURE__ */ new Float64Array([1]);
|
|
125
130
|
/** Camera equation H = 0.65·L·t/N² (CIPA DC-004-2020 §5.5 (4); 0.65 = τ·π/4 with τ = 0.828, Explanation 4). */
|
|
126
131
|
const CAMERA_EQUATION_CONSTANT = 0.65;
|
|
127
132
|
/** Neutral reference reflectance: DC-004 ties the SOS reference level to an 18 % grey (Explanation 1 and 2.2). */
|
|
@@ -179,15 +184,16 @@ function greyCardDeviationStops(fNumber, shutterSeconds, iso, illuminanceLux) {
|
|
|
179
184
|
*/
|
|
180
185
|
export const SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX = Math.PI * sosReferenceLuminance(DEFAULTS.fNumber, DEFAULTS.shutterSeconds, DEFAULTS.iso) /
|
|
181
186
|
GREY_CARD_REFLECTANCE;
|
|
182
|
-
/**
|
|
183
|
-
* Sensitive-aperture area (µm²) at which the tier's ISO signal chain applies unchanged: the default 6 µm square
|
|
184
|
-
* aperture with fill factor 1. At other areas the facade scales the electron-domain readout values by
|
|
185
|
-
* area / this reference, so the same focal-plane exposure H gives the same output code (see `isoAreaScaledReadout`).
|
|
186
|
-
*/
|
|
187
187
|
/** 2.5: the largest accepted pixel pitch, µm (44 mm ÷ 48 pixels ≈ 917 µm, a whole full-frame or larger sensor at a small output). */
|
|
188
188
|
const SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS = 1000;
|
|
189
189
|
/** Pitches above this, µm, are larger than any camera photosite: the capture is a downsized whole-frame view (assumption sensor.wholeFrameView). */
|
|
190
190
|
const SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS = 100;
|
|
191
|
+
/**
|
|
192
|
+
* Sensitive-aperture area (µm²) at which the tier's ISO signal chain applies unchanged: the default 6 µm square
|
|
193
|
+
* aperture with fill factor 1. At other areas the facade scales the electron-domain readout values by
|
|
194
|
+
* area / this reference, so the same focal-plane exposure H gives the same output code (the `chargeScale` of
|
|
195
|
+
* `characterizeReadout`, src/sensor/sensor-characteristics.ts).
|
|
196
|
+
*/
|
|
191
197
|
const ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS = DEFAULTS.pixelPitchMicrometers * DEFAULTS.pixelPitchMicrometers;
|
|
192
198
|
/** Demosaic border, native sites per side. */
|
|
193
199
|
const MARGIN = 1;
|
|
@@ -340,17 +346,6 @@ const TEXT_METADATA_LAYOUT = /* @__PURE__ */ Object.freeze({
|
|
|
340
346
|
png: "One tEXt (printable Latin-1) or iTXt (UTF-8) chunk per field, keywords Description, Software, Copyright, Comment, before the first IDAT",
|
|
341
347
|
dng: "ImageDescription (statement, then the description) and Copyright when printable ASCII; an XMP packet in tag 700 with every field"
|
|
342
348
|
});
|
|
343
|
-
/**
|
|
344
|
-
* The readout profile with every regime's charge-domain transfer scaled by `areaScale`: system conversion gain
|
|
345
|
-
* (e⁻ per code) and pre-ADC saturation (e⁻ equivalent) are multiplied by it, so codes per unit focal-plane exposure
|
|
346
|
-
* stay those of the reference aperture. Read noise stays in electrons per pixel; codes, black level, ADC and digital
|
|
347
|
-
* gain are unchanged. The result is re-checked by the readout parser.
|
|
348
|
-
*/
|
|
349
|
-
function isoAreaScaledReadout(profile, areaScale) {
|
|
350
|
-
return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => ({ ...regime,
|
|
351
|
-
systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: regime.systemConversionGainElectronsPerCode.value * areaScale },
|
|
352
|
-
preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * areaScale } })) });
|
|
353
|
-
}
|
|
354
349
|
/** Diagnostic clock for the 2.1 stage timings: `performance.now()` where the host has it. Never affects output. */
|
|
355
350
|
export function clockMs() { return globalThis.performance?.now() ?? Date.now(); }
|
|
356
351
|
function positive(value, name) {
|
|
@@ -744,7 +739,7 @@ export async function serializeSimulatedCaptureDngFromParts(options, parts) {
|
|
|
744
739
|
return await runCapture(options, "dng-from-parts", parts);
|
|
745
740
|
}
|
|
746
741
|
/** The whole frame's packed RAW from region parts (see `serializeSimulatedCaptureDngFromParts`). */
|
|
747
|
-
function joinNativeRawParts(value, plan, width, height, caller) {
|
|
742
|
+
function joinNativeRawParts(value, plan, width, height, caller, fast) {
|
|
748
743
|
if (!Array.isArray(value) || value.length === 0)
|
|
749
744
|
throw new InvalidConfigurationError(caller + " needs at least one part.");
|
|
750
745
|
const parts = value, count = width * height;
|
|
@@ -755,6 +750,8 @@ function joinNativeRawParts(value, plan, width, height, caller) {
|
|
|
755
750
|
const rect = part?.region?.nativeRect, raw = part?.raw;
|
|
756
751
|
if (!rect || !raw)
|
|
757
752
|
bad(k, "needs raw and region.nativeRect (a region render's result).");
|
|
753
|
+
if (part.fast === true && !fast)
|
|
754
|
+
bad(k, "a fast method rendered it, but this request resolves to the exact methods (fast: false, the default for a DNG, JPEG or PNG). Render the parts with fast: false, or pass fast: true here.");
|
|
758
755
|
const { x, y, width: w, height: h } = rect;
|
|
759
756
|
if (![x, y, w, h].every(Number.isSafeInteger) || x < 0 || y < 0 || w < 1 || h < 1 || x + w > width || y + h > height)
|
|
760
757
|
bad(k, "its nativeRect is not inside the frame's native raster.");
|
|
@@ -1148,11 +1145,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1148
1145
|
fail("lens.defocus cannot be combined with lens.pupilSampling: the pupil rays already render defocus, so it would be counted twice.");
|
|
1149
1146
|
if (lensPsf)
|
|
1150
1147
|
fail("lens.defocus cannot be combined with lens.psf: a caller PSF declares its own defocus, so it could be counted twice.");
|
|
1151
|
-
// #549: with a catalogue lens
|
|
1152
|
-
// options (#553) shape the lens.defocus kernel only, so they are refused rather than dropped (a follow-up for a later release).
|
|
1148
|
+
// #549, #567: with a catalogue lens its own bokeh kernel is convolved into its PSF (below); it sets all three bokeh options.
|
|
1153
1149
|
if (catalogue && defocusBokeh)
|
|
1154
1150
|
fail("lens.defocus.blades, sphericalAberration and catsEye cannot be combined with lens.catalogue: the catalogue lens " +
|
|
1155
|
-
"
|
|
1151
|
+
"sets its own (lens.catalogue.character). Omit the bokeh options, or use lens.focalLengthMm instead of the catalogue lens.");
|
|
1156
1152
|
if (!catalogue)
|
|
1157
1153
|
defocusModel = "geometric-disc";
|
|
1158
1154
|
}
|
|
@@ -1190,10 +1186,29 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1190
1186
|
const format = pick(outputOptions, "format", "output.format", DEFAULTS.format, "8-bit sRGB samples.");
|
|
1191
1187
|
if (!["srgb-8", "srgb-16", "jpeg", "png", "dng"].includes(format))
|
|
1192
1188
|
fail("output.format must be srgb-8, srgb-16, jpeg, png or dng.");
|
|
1193
|
-
|
|
1189
|
+
const correctionSelection = (value, name) => {
|
|
1190
|
+
if (value === undefined)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
const o = allowKeys(value, name, ["distortion", "lateralColor", "vignetting"]), selection = {};
|
|
1193
|
+
for (const key of ["distortion", "lateralColor", "vignetting"]) {
|
|
1194
|
+
if (o[key] !== undefined && typeof o[key] !== "boolean")
|
|
1195
|
+
fail(name + "." + key + " must be true or false.");
|
|
1196
|
+
selection[key] = o[key] === true;
|
|
1197
|
+
}
|
|
1198
|
+
if (!Object.values(selection).includes(true))
|
|
1199
|
+
return undefined;
|
|
1200
|
+
if (!catalogue)
|
|
1201
|
+
fail(name + " needs lens.catalogue: the corrections are the catalogue lens's own profile.");
|
|
1202
|
+
return selection;
|
|
1203
|
+
};
|
|
1204
|
+
let dngCapturedAtUtc, dngOpcodeSelection;
|
|
1194
1205
|
if (format === "dng") {
|
|
1195
|
-
const d = allowKeys(outputOptions.dng, "output.dng", ["capturedAtUtc"]);
|
|
1206
|
+
const d = allowKeys(outputOptions.dng, "output.dng", ["capturedAtUtc", "lensCorrectionOpcodes"]);
|
|
1196
1207
|
dngCapturedAtUtc = d.capturedAtUtc;
|
|
1208
|
+
// #587: the corrections as DNG opcodes, which apply to the whole frame's ActiveArea.
|
|
1209
|
+
dngOpcodeSelection = correctionSelection(d.lensCorrectionOpcodes, "output.dng.lensCorrectionOpcodes");
|
|
1210
|
+
if (dngOpcodeSelection && outputOptions.region !== undefined)
|
|
1211
|
+
fail("output.dng.lensCorrectionOpcodes needs the whole frame (no output.region).");
|
|
1197
1212
|
if (dngCapturedAtUtc === undefined)
|
|
1198
1213
|
assume("output.dng.capturedAtUtc", "default", null, "No capture time: the DNG carries no DateTimeOriginal (the engine never invents a wall clock).");
|
|
1199
1214
|
}
|
|
@@ -1249,10 +1264,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1249
1264
|
}
|
|
1250
1265
|
const exposureGain = exposureCompensationStops === 0 ? 1 : 2 ** exposureCompensationStops;
|
|
1251
1266
|
const whiteBalanceOption = pick(outputOptions, "whiteBalance", "output.whiteBalance", DEFAULTS.whiteBalance, "White balance measured from a perfect white diffuser under the scene illuminant (a custom/grey-card white balance).");
|
|
1252
|
-
const jpegStep = format === "jpeg" ? integerIn(pick(outputOptions, "jpegQuantizationStep", "output.jpegQuantizationStep", DEFAULTS.jpegQuantizationStep, "Constant JPEG quantization step
|
|
1267
|
+
const jpegStep = format === "jpeg" ? integerIn(pick(outputOptions, "jpegQuantizationStep", "output.jpegQuantizationStep", DEFAULTS.jpegQuantizationStep, "Constant JPEG quantization step 1 (not a manufacturer quality scale)."), "output.jpegQuantizationStep", 1, 255) : undefined;
|
|
1253
1268
|
if (format !== "jpeg" && outputOptions.jpegQuantizationStep !== undefined)
|
|
1254
1269
|
fail("output.jpegQuantizationStep applies only to output.format \"jpeg\".");
|
|
1255
|
-
const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing", "pixelAperture"]);
|
|
1270
|
+
const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing", "pixelAperture", ...SENSOR_CHARACTERISTIC_KEYS]);
|
|
1256
1271
|
const pitch = positive(pick(sensorOptions, "pixelPitchMicrometers", "sensor.pixelPitchMicrometers", DEFAULTS.pixelPitchMicrometers, "6 µm pixels (about a 24 MP full-frame sensor). The simulated sensor is width × pitch wide, so small images are a central crop of a large sensor, not a downscaled full frame."), "sensor.pixelPitchMicrometers");
|
|
1257
1272
|
if (pitch < 0.5 || pitch > SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS)
|
|
1258
1273
|
fail("sensor.pixelPitchMicrometers must be 0.5-" + SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS + ".");
|
|
@@ -1261,6 +1276,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1261
1276
|
fail("sensor.spectralStepNanometers must be 1-100.");
|
|
1262
1277
|
const temporalSampleCount = integerIn(rotating ? pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", ROTATING_TEMPORAL_SAMPLE_COUNT, "Eight temporal midpoints across each site's exposure window sample the camera rotation (motion blur); raise it for long exposures or fast rotation.") :
|
|
1263
1278
|
pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", DEFAULTS.temporalSampleCount, "One temporal sample: the static scene and still camera make more samples unnecessary."), "sensor.temporalSampleCount", 1, 256);
|
|
1279
|
+
const characteristics = parseSensorCharacteristics(sensorOptions);
|
|
1264
1280
|
const noiseSeed = integerIn(pick(sensorOptions, "noiseSeed", "sensor.noiseSeed", DEFAULTS.noiseSeed, "Fixed noise seed 1 (deterministic output)."), "sensor.noiseSeed", 0, 0xffffffff);
|
|
1265
1281
|
const antiAliasing = sensorOptions.antiAliasing === undefined ? undefined : parseAntiAliasing(sensorOptions.antiAliasing, pitch);
|
|
1266
1282
|
const antiAliasingFast = antiAliasing ? fastOverride(sensorOptions.antiAliasing, "sensor.antiAliasing") : undefined;
|
|
@@ -1328,10 +1344,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1328
1344
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
1329
1345
|
fail("output.demosaic must be bilinear or ahd.");
|
|
1330
1346
|
const ahd = demosaicOption === "ahd";
|
|
1331
|
-
// #395: exact or fast per option
|
|
1347
|
+
// #395: exact or fast per option: the option record's `fast`, else the top-level one, else (2.20.0) the format's default: fast for
|
|
1348
|
+
// sRGB samples and the display view, exact for the exports (DNG, JPEG, PNG) and the ideal reference. Fast methods report true when they run.
|
|
1332
1349
|
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1333
1350
|
fail("fast must be true or false.");
|
|
1334
|
-
const fastTop = top.fast
|
|
1351
|
+
const fastTop = resolveFast(top.fast, fastFormatDefault(format, displayView ? "display-view" : idealReference ? "ideal-reference" : "capture"));
|
|
1335
1352
|
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
1336
1353
|
["lens.catalogue", catalogue !== undefined, catalogueFast],
|
|
1337
1354
|
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast]];
|
|
@@ -1340,11 +1357,15 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1340
1357
|
if (used)
|
|
1341
1358
|
fastApplied[option] = false;
|
|
1342
1359
|
// #568: the catalogue's fast method, when requested (its record's flag, else the top-level one); whether it runs is decided with the RAW path.
|
|
1343
|
-
const catalogueFastRequested = catalogue !== undefined && (catalogueFast
|
|
1360
|
+
const catalogueFastRequested = catalogue !== undefined && resolveFast(catalogueFast, fastTop);
|
|
1344
1361
|
let fastReport = Object.freeze({
|
|
1345
1362
|
requested: fastTop || [diffractionFast, catalogueFast, antiAliasingFast, boxFast, demosaicFast].some((value) => value === true),
|
|
1346
|
-
applied: Object.freeze({ ...fastApplied }), assurance: "engine-reference"
|
|
1363
|
+
defaultedFrom: top.fast === undefined ? "format" : "request", applied: Object.freeze({ ...fastApplied }), assurance: "engine-reference"
|
|
1347
1364
|
});
|
|
1365
|
+
const markFast = (option) => {
|
|
1366
|
+
fastApplied[option] = true;
|
|
1367
|
+
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
1368
|
+
};
|
|
1348
1369
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
1349
1370
|
fail("output.borderHandling must be margin or mirror.");
|
|
1350
1371
|
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
@@ -1358,22 +1379,12 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1358
1379
|
// Geometry: the demosaic border (if any) on each side; output crop is the active area.
|
|
1359
1380
|
const nativeWidth = width + 2 * margin, nativeHeight = height + 2 * margin;
|
|
1360
1381
|
// Lens corrections (#570): the catalogue lens's corrections on the developed linear colour; all off is no option at all.
|
|
1361
|
-
let lensCorrection
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
selection[key] = o[key] === true;
|
|
1368
|
-
}
|
|
1369
|
-
if (keys.some((key) => selection[key])) {
|
|
1370
|
-
if (idealReference)
|
|
1371
|
-
fail("simulateIdealReference does not take output.lensCorrections: a reference has no camera development.");
|
|
1372
|
-
if (!catalogue || !lensKit)
|
|
1373
|
-
fail("output.lensCorrections needs lens.catalogue: the corrections are the catalogue lens's own profile.");
|
|
1374
|
-
lensCorrectionSelection = selection;
|
|
1375
|
-
lensCorrection = lensKit.prepareDevelopedLensCorrection(catalogue, selection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
1376
|
-
}
|
|
1382
|
+
let lensCorrection;
|
|
1383
|
+
const lensCorrectionSelection = correctionSelection(outputOptions.lensCorrections, "output.lensCorrections");
|
|
1384
|
+
if (lensCorrectionSelection) {
|
|
1385
|
+
if (idealReference)
|
|
1386
|
+
fail("simulateIdealReference does not take output.lensCorrections: a reference has no camera development.");
|
|
1387
|
+
lensCorrection = lensKit.prepareDevelopedLensCorrection(catalogue, lensCorrectionSelection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
1377
1388
|
}
|
|
1378
1389
|
// Region (2.3): a rectangle of the whole frame. Its native sites are the region plus the demosaic reach on each side
|
|
1379
1390
|
// (clipped to the raster, where mirror borders apply exactly as in the whole render), with the origin rounded down to
|
|
@@ -1746,7 +1757,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1746
1757
|
// its distortion and vignetting profiles over the field the PSF and the queries reach, and its PSF profile.
|
|
1747
1758
|
let catalogueTaps, catalogueDefocusApplied = false, catalogueKernelRadius = 0;
|
|
1748
1759
|
if (catalogue && lensKit) {
|
|
1749
|
-
const { GENERIC_LENS_EVIDENCE, genericLensDistortionProfile, genericLensVignettingProfile, resolveGenericLens, simulateGenericLensFocusError, createGenericLensPsfProfile, genericLensPsfKernelRadius, GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES } = lensKit;
|
|
1760
|
+
const { GENERIC_LENS_EVIDENCE, genericLensDistortionProfile, genericLensVignettingProfile, resolveGenericLens, simulateGenericLensFocusError, createGenericLensPsfProfile, genericLensPsfKernelRadius, GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES, genericLensBokehKernel } = lensKit;
|
|
1750
1761
|
checkFNumber();
|
|
1751
1762
|
if (lensOptions.focusScatter !== undefined) {
|
|
1752
1763
|
const scatterOptions = allowKeys(lensOptions.focusScatter, "lens.focusScatter", ["seed", "shotIndex"]);
|
|
@@ -1768,16 +1779,25 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1768
1779
|
catch {
|
|
1769
1780
|
fail("lens.defocus: " + defocusDiscLimitMessage(disc.diameterSites, disc.diameterMicrometers));
|
|
1770
1781
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1782
|
+
// #567: the lens's bokeh kernel (blades, its W040, a cat's eye) instead of the plain disc.
|
|
1783
|
+
// The cat's eye (its finer fast grid) is lens.catalogue's exact method; fast leaves it out.
|
|
1784
|
+
const catsEye = !catalogueFastRequested;
|
|
1785
|
+
if (run !== "ideal-reference" && disc.diameterSites > 0) {
|
|
1786
|
+
extraKernel = genericLensBokehKernel(character, disc.diameterSites, subjectDistanceM > focusDistanceM ? 1 : -1, pitch, catsEye);
|
|
1774
1787
|
catalogueDefocusApplied = true;
|
|
1788
|
+
if (!catsEye && character.character.bokeh.catsEye) {
|
|
1789
|
+
markFast("lens.catalogue");
|
|
1790
|
+
assume("fast.lens.catalogue.catsEye", "model", { method: "bokeh-without-cats-eye" }, "Fast: the lens's bokeh leaves out its cat's eye (the pupil clipped by field position), " +
|
|
1791
|
+
"so the kernel is the axis kernel everywhere; the exact method (fast: false, the default for DNG, JPEG and PNG) shapes it at each field point.");
|
|
1792
|
+
}
|
|
1775
1793
|
}
|
|
1794
|
+
const k = extraKernel?.axis;
|
|
1776
1795
|
assume("lens.defocus", "model", { model: "geometric-disc", scientificStatus: "approximation", diameterMicrometers: disc.diameterMicrometers, diameterSites: disc.diameterSites,
|
|
1777
|
-
kernelRadiusSites: disc.diameterSites === 0 ? null : discRadius, fNumber, focusDistanceM, subjectDistanceM, samplePitchMicrometers: pitch,
|
|
1778
|
-
defocusCircleModel: { id: "thin-lens-defocus-circle", version: "1.1.0" }, execution: "catalogue-lens-psf"
|
|
1796
|
+
kernelRadiusSites: disc.diameterSites === 0 ? null : k ? k.radius : discRadius, fNumber, focusDistanceM, subjectDistanceM, samplePitchMicrometers: pitch,
|
|
1797
|
+
defocusCircleModel: { id: "thin-lens-defocus-circle", version: "1.1.0" }, execution: "catalogue-lens-psf",
|
|
1798
|
+
...(k ? { bokeh: { catsEye: catsEye && !!character.character.bokeh.catsEye, method: k.method, sphericalAberrationSites: k.sphericalAberrationSites, axisOpenAreaRatio: k.openAreaRatio } } : {}) }, "Geometric defocus of the flat scene plane (an approximation): the thin-lens blur circle c = f²·|s − d| / (N·d·(s − f)) (calculateDefocusCircle 1.1.0) as a uniform disc sampled on the site lattice. " +
|
|
1779
1799
|
(disc.diameterSites === 0 ? "In focus: no disc is applied." : run === "ideal-reference" ? "Resolved but not applied: the ideal reference has no camera effects." :
|
|
1780
|
-
"With the catalogue lens the disc is convolved into every node of the lens's PSF (a further approximation: the defocused aberrated wave PSF is not exactly the in-focus PSF convolved with the disc) and applied through the environment PSF quadrature."));
|
|
1800
|
+
"With the catalogue lens the disc is shaped by the lens's bokeh (#567, #553's kernel; see lens.catalogue.character) and convolved into every node of the lens's PSF (a further approximation: the defocused aberrated wave PSF is not exactly the in-focus PSF convolved with the disc) and applied through the environment PSF quadrature."));
|
|
1781
1801
|
}
|
|
1782
1802
|
// An upper bound of the PSF's radius (the built window may be narrower): the field reach below uses it.
|
|
1783
1803
|
const kernelRadius = genericLensPsfKernelRadius({ lens: character, focusDistanceM, sitePitchMicrometers: pitch }) + (extraKernel ? extraKernel.radius : 0);
|
|
@@ -1821,23 +1841,32 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1821
1841
|
}
|
|
1822
1842
|
assume("lens.catalogue.character", "model", { lensId: catalogue.lensId, fNumber, focusDistanceM, magnification: c.magnification, closeFocusGain: c.closeFocusGain, pupilScale: c.pupilScale,
|
|
1823
1843
|
aberrationsWaves550: { ...c.aberrations }, lateralColor: { ...c.lateralColor }, distortionCorner: c.distortion.corner, vignettingCornerStops: c.vignetting.cornerStops,
|
|
1824
|
-
notRendered: { strayLight: { ...c.strayLight }, breathing: { ...c.breathing }, focusScatterDepthOfFocusFraction: c.focusScatterDepthOfFocusFraction
|
|
1844
|
+
bokeh: c.bokeh, notRendered: { strayLight: { ...c.strayLight }, breathing: { ...c.breathing }, focusScatterDepthOfFocusFraction: c.focusScatterDepthOfFocusFraction },
|
|
1825
1845
|
psf: psfReport, profiles: { vignetting, distortion: distortion.profile, maximumNormalizedRadius: reachRadius } }, "The catalogue lens's character, a labelled approximation of a generic lens (docs/LENS_CATALOGUE.md): Seidel wave aberrations at the frame corner (spherical, balanced by defocus; coma; astigmatism; field curvature), " +
|
|
1826
1846
|
"scaled from the widest aperture by the pupil ratio a = N0/N (a⁴, a³, a², a²) and by the close-focus factor; axial colour as a defocus and lateral colour as a radial shift, both with the normalised Cauchy dispersion. " +
|
|
1827
1847
|
(run === "ideal-reference" ? "Not applied: the ideal reference has no camera effects. " :
|
|
1828
1848
|
"Rendered as a sampled PSF (scalar Fraunhofer diffraction of the aberrated circular pupil, integrated over each site cell, on a 5 × 5 field grid × 5 wavelengths with centroid-aligned interpolation) " +
|
|
1829
1849
|
"through the environment PSF quadrature (one scene query per kernel tap), with the lens's radial distortion (r_d = r·(1 + k1ρ²)) and relative illumination (E = (1 − bρ²)²) as lens field mapping and field throughput" +
|
|
1830
1850
|
(catalogue.kind === "fisheye" ? ", and the equisolid projection; directions at or beyond 90° see no scene (zero radiance), so a circular fisheye's image ends at its 180° circle" : "") + ". ") +
|
|
1831
|
-
"Flare
|
|
1851
|
+
"Flare and focus breathing are reported, not rendered; the bokeh shapes lens.defocus; ρ is normalised to the frame's half-diagonal, so the lens is scaled to the simulated frame like lens.prime.");
|
|
1832
1852
|
}
|
|
1833
1853
|
// #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.
|
|
1834
1854
|
// #548: a lens field mapping, vignetting or camera rotation breaks that, so the capture then applies the disc in image space, on the sensor's
|
|
1835
1855
|
// site lattice (defocus-image-space-provider.ts). #563: the display view reads the same geometry in any focus (display-view-lens-signal.ts).
|
|
1836
1856
|
const imageSpaceOptions = [...(lensFieldMapping ? [lensOptions.distortion !== undefined ? (lensOptions.projection !== undefined ? "lens.projection, lens.distortion" : "lens.distortion") : "lens.projection"] : []),
|
|
1837
1857
|
...(vignetting ? ["lens.vignetting"] : []), ...(rotating ? ["camera rotation"] : [])];
|
|
1838
|
-
const lensGeometry = (defocusModel !== undefined || displayView) && imageSpaceOptions.length > 0 ? prepareDefocusImageSpaceGeometry({ focalLengthMm, focusDistanceM,
|
|
1858
|
+
const lensGeometry = (defocusModel !== undefined || displayView || diffractionRadiusOption !== undefined) && imageSpaceOptions.length > 0 ? prepareDefocusImageSpaceGeometry({ focalLengthMm, focusDistanceM,
|
|
1839
1859
|
angularVelocityRadPerSec: angular, ...(lensFieldMapping ? { lensMapping: prepareEnvironmentLensFieldMapping(lensFieldMapping) } : {}), ...(vignetting ? { vignetting } : {}) }) : undefined;
|
|
1840
1860
|
const defocusGeometry = defocusModel !== undefined ? lensGeometry : undefined;
|
|
1861
|
+
// #565: the in-focus PSF quadrature reads each tap's source on this lattice; with rotation under a rolling shutter (each row has its own
|
|
1862
|
+
// instants, so a band per instant, about 1.2× the time) that is lens.diffraction's exact method, and each tap's own point its fast one.
|
|
1863
|
+
const rollingFastTaps = !displayView && rotating && shutterKind === "rolling" && diffractionRadiusOption !== undefined && lensGeometry !== undefined && resolveFast(diffractionFast, fastTop);
|
|
1864
|
+
if (rollingFastTaps) {
|
|
1865
|
+
markFast("lens.diffraction");
|
|
1866
|
+
assume("fast.lens.diffraction", "model", { method: "per-tap-source-points" }, "Fast: under rolling-shutter rotation each PSF tap reads the scene at its own source point, " +
|
|
1867
|
+
"not at its site-lattice point, so a tap exactly on a chart boundary can resolve to the other material (#565); elsewhere the values agree to rounding. The exact method is fast: false.");
|
|
1868
|
+
}
|
|
1869
|
+
const latticeTaps = lensGeometry && !rollingFastTaps;
|
|
1841
1870
|
// #553: the shaped kernel (on axis) and, with a cat's eye, its field zones; built when the disc is resolved out of focus.
|
|
1842
1871
|
let bokehAxisKernel, bokehZones;
|
|
1843
1872
|
const bokehSummary = defocusBokeh === undefined ? undefined : (disc) => {
|
|
@@ -1912,18 +1941,23 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1912
1941
|
// (EMVA 1288 Release 4.0, Eq. 3). The tier's signal chain is anchored to the 6 µm aperture; at another area the
|
|
1913
1942
|
// charge-domain readout scales with the area so the same H gives the same code. At 6 µm nothing is changed.
|
|
1914
1943
|
const areaScale = pitch * pitch / ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS;
|
|
1944
|
+
// #555: a full well replaces the area-scaled base-ISO saturation; the charge-domain transfer scales by the same rule.
|
|
1945
|
+
const fullWell = characteristics?.fullWell, fullWellElectrons = fullWell && (fullWell.link === "none" ? fullWell.electrons : fullWell.electrons * (pitch / (fullWell.referencePixelPitchMicrometers ?? 6)) ** 2);
|
|
1946
|
+
if (fullWellElectrons !== undefined && !(fullWellElectrons >= 100 && fullWellElectrons <= 1e10))
|
|
1947
|
+
fail("sensor.fullWell must give 100 to 1e10 electrons at the pixel pitch (" + fullWellElectrons + ").");
|
|
1948
|
+
const chargeScale = fullWellElectrons === undefined ? areaScale : fullWellElectrons / body.readout[0].profile.regimes[0].preAdcSaturationElectronEquivalent.value;
|
|
1915
1949
|
const regimeByChannel = {};
|
|
1916
1950
|
for (const channel of CHANNELS) {
|
|
1917
1951
|
const asset = body.readout.find((a) => a.profile.channelId === channel) ?? fail("Camera readout lacks channel " + channel + ".");
|
|
1918
1952
|
const chain = resolveGenericIsoSignalChain({ profile: body.signalChain, isoCapability: body.iso, requestedIsoSetting: { kind: "standard", exposureIndex: resolvedIso },
|
|
1919
1953
|
captureModeId: IDS.mode, readoutProfile: asset.profile });
|
|
1920
|
-
regimeByChannel[channel] = { readoutProfile:
|
|
1954
|
+
regimeByChannel[channel] = { readoutProfile: characterizeReadout(asset.profile, chargeScale, characteristics), regimeId: chain.readout.regime.regimeId };
|
|
1921
1955
|
}
|
|
1922
|
-
const capacityRegimes = (
|
|
1956
|
+
const capacityRegimes = characterizeReadout(body.readout[0].profile, chargeScale, characteristics).regimes;
|
|
1923
1957
|
const baseRegime = capacityRegimes[0];
|
|
1924
1958
|
const capacityElectrons = Math.max(...capacityRegimes.map((r) => r.preAdcSaturationElectronEquivalent.value), baseRegime.preAdcSaturationElectronEquivalent.value);
|
|
1925
1959
|
assume("sensor.capacityElectrons", "derived", capacityElectrons, "Physical charge capacity set to the body's largest declared pre-ADC saturation (its base-ISO saturation signal), so storage never clips before the declared readout white.");
|
|
1926
|
-
if (areaScale !== 1) {
|
|
1960
|
+
if (areaScale !== 1 && !fullWell) {
|
|
1927
1961
|
assume("sensor.isoAreaNormalization", "model", { referenceApertureAreaSquareMicrometers: ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS,
|
|
1928
1962
|
apertureAreaSquareMicrometers: pitch * pitch, areaScale }, "Approximation. The tier's ISO signal chain is declared for a 6 µm square aperture. Standard output sensitivity S = 10/H (CIPA DC-004-2020 §3.2) is defined at the output, " +
|
|
1929
1963
|
"independent of pixel size, and the collected charge is proportional to the aperture area (EMVA 1288 Release 4.0, Eq. 3), so the system conversion gain (e⁻ per code) and the " +
|
|
@@ -1932,6 +1966,15 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1932
1966
|
"the aperture area: a design consequence of holding the tier's ISO rating and highlight headroom fixed, not a measured full-well model (published sensor data grow less than " +
|
|
1933
1967
|
"proportionally with area across technologies).");
|
|
1934
1968
|
}
|
|
1969
|
+
if (characteristics) {
|
|
1970
|
+
const density = capacityElectrons / (pitch * pitch);
|
|
1971
|
+
assume("sensor.characteristics", "model", { ...Object.fromEntries(SENSOR_CHARACTERISTIC_KEYS.filter((k) => sensorOptions[k] !== undefined).map((k) => [k, sensorOptions[k]])),
|
|
1972
|
+
chargeScale, sensitivityOffsetStops: Math.log2(characteristics.qeScale * areaScale / chargeScale) }, "Approximation (#555), not a measured sensor: these replace the body's values. The full well is the base-ISO charge at the RAW white level (EMVA 1288 Release 4.0 saturation capacity); " +
|
|
1973
|
+
"every ISO regime's conversion gain and pre-ADC saturation are multiplied by chargeScale. Each ISO keeps its regime, so the output per unit exposure changes by 2^sensitivityOffsetStops: " +
|
|
1974
|
+
"the rating S = 10/H (CIPA DC-004-2020 §3.2) holds only if peak QE × pixel area / full well is the body's. Read noise replaces each regime's; the high gain's smaller storage is not modelled. " +
|
|
1975
|
+
"A bit depth scales the black and saturation codes by 2^Δbits." + (density < 200 || density > 1e4 ? " Unusual for the pitch: " + density +
|
|
1976
|
+
" e⁻/µm², outside 200–10 000 (a design range around the tiers' 556–1 667, not a published bound)." : ""));
|
|
1977
|
+
}
|
|
1935
1978
|
// Whole-frame views (2.5): a pitch above 100 µm is larger than any camera photosite, so the capture is a downsized view of a frame width × pitch wide.
|
|
1936
1979
|
if (pitch > SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS) {
|
|
1937
1980
|
assume("sensor.wholeFrameView", "model", { pixelPitchMicrometers: pitch, frameWidthMm: width * pitch / 1000, frameHeightMm: height * pitch / 1000 }, "A pixel pitch above 100 µm is larger than any camera photosite, so each output pixel stands for a " + pitch + " µm square of a " + width * pitch / 1000 + " × " + height * pitch / 1000 +
|
|
@@ -1965,7 +2008,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1965
2008
|
// Shutter (2.0): the electronic rolling-shutter schedule derived from the body's rolling readout declaration.
|
|
1966
2009
|
let rollingSchedule;
|
|
1967
2010
|
if (shutterKind === "rolling") {
|
|
1968
|
-
const
|
|
2011
|
+
const bodyReadout = body.rollingReadoutTiming?.profile;
|
|
2012
|
+
// #555: sensor.readoutSeconds replaces the body's readout time and skew.
|
|
2013
|
+
const seconds = characteristics?.readoutSeconds, declared = bodyReadout && seconds ? { ...bodyReadout, captureReadoutDurationSeconds: { ...bodyReadout.captureReadoutDurationSeconds, value: seconds },
|
|
2014
|
+
spatialSamplingSkewSeconds: { ...bodyReadout.spatialSamplingSkewSeconds, value: seconds } } : bodyReadout;
|
|
1969
2015
|
if (!declared || declared.readoutMode !== "rolling")
|
|
1970
2016
|
fail("motion.shutter \"rolling\" needs a camera body with a rolling readout timing declaration.");
|
|
1971
2017
|
const readout = rollingSkewOption === undefined ? declared : { ...declared, spatialSamplingSkewSeconds: { value: rollingSkewOption, unit: "s", evidence: FACADE_EVIDENCE } };
|
|
@@ -2074,7 +2120,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2074
2120
|
const eqeSamples = (channel) => {
|
|
2075
2121
|
const out = [];
|
|
2076
2122
|
for (let nm = range.minimum; nm <= range.maximum + 1e-9; nm += eqeSpacing)
|
|
2077
|
-
out.push({ nm, value: EQE_SCALE[channel] * estimateCie1931ColorMatchingFunctions(nm)[CMF_KEY[channel]] * 555 / nm });
|
|
2123
|
+
out.push({ nm, value: EQE_SCALE[channel] * estimateCie1931ColorMatchingFunctions(nm)[CMF_KEY[channel]] * 555 / nm * (characteristics?.qeScale ?? 1) });
|
|
2078
2124
|
return out;
|
|
2079
2125
|
};
|
|
2080
2126
|
const operatingConditions = { temperatureC: 25, incidenceAngleDegreesFromNormal: 0, polarization: "unpolarized" };
|
|
@@ -2138,9 +2184,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2138
2184
|
samples: { value: [{ wavelengthNanometers: range.minimum, linearTransmissionFactor: lens.matchedReference.transmission },
|
|
2139
2185
|
{ wavelengthNanometers: range.maximum, linearTransmissionFactor: lens.matchedReference.transmission }], evidence: preset.evidence },
|
|
2140
2186
|
uncertainty: { kind: "not-quantified", limitation: "Synthetic spectrally flat transmission from the tier preset." } },
|
|
2141
|
-
|
|
2142
|
-
sensorOpticalStackIncluded: false, strayLightIncluded: false, polarizationModeled: false, wavelengthChangingBehaviorModeled: false, volumetricScatteringModeled: false,
|
|
2143
|
-
evidence: preset.evidence, limitations: [...lens.limitations, "Flat transmission applied across the lens capability by the capture facade."] };
|
|
2187
|
+
...SCENE_TO_SENSOR_BRIDGE_SCOPE, evidence: preset.evidence, limitations: [...lens.limitations, "Flat transmission applied across the lens capability by the capture facade."] };
|
|
2144
2188
|
const optics = { profile: opticsProfile, focalLengthMm, nominalFNumber: fNumber,
|
|
2145
2189
|
focus: { kind: "ideal-symmetric-thin-lens", objectDistanceM: focusDistanceM, pupilMagnificationAssumption: "unity" } };
|
|
2146
2190
|
const motion = { angularVelocityRadPerSec: angular, timeReference: "first-opening-boundary-phase", environmentDirectionConvention: "outgoing-radiance-toward-camera" };
|
|
@@ -2185,7 +2229,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2185
2229
|
(diffractionObjectSpace ? "Applied as an object-space convolution: the scene radiance on the site lattice is convolved once per spectral node (calculateIrradianceConvolution) and the provider answers each site query with it, so the site inputs declare no PSF; this equals the per-tap environment PSF quadrature for this still, distortion-free planar scene (request.diffraction.psf). " :
|
|
2186
2230
|
defocusImageSpace ? "Applied with lens.defocus as one image-space kernel per spectral node (the diffraction kernel convolved with the disc) on the sensor's site lattice (#548; see the lens.defocus assumption), " +
|
|
2187
2231
|
"the same sum as the environment PSF quadrature with that kernel. " :
|
|
2188
|
-
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice. ") +
|
|
2232
|
+
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice" + (latticeTaps ? "; each tap reads the scene at its lattice point (#565). " : ". ")) +
|
|
2189
2233
|
"No defocus, aberration or pupil clipping; " + (boxAperture ? "the pixel aperture is integrated as declared by sensor.pixelAperture." : "the sensor pixel aperture is still point-sampled at the site centre."));
|
|
2190
2234
|
}
|
|
2191
2235
|
// #492: each spectral node's kernel for lens.defocus out of focus: the disc, or with lens.diffraction the node's diffraction kernel convolved
|
|
@@ -2258,6 +2302,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2258
2302
|
limitation: "Ideal uniform circular pupil; finite ray sampling, no convergence claim." } : undefined;
|
|
2259
2303
|
const chargeSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.chargeSampling, completenessProfileId: IDS.completeness, scientificStatus: "approximation",
|
|
2260
2304
|
photoShotNoiseModel: "poisson", darkShotNoiseModel: "poisson", additionalComponentPolicies: [], evidence, limitations: ["Poisson photo and dark shot noise."] };
|
|
2305
|
+
// #555: the body's fixed-pattern noise profile, opt-in; the display view's expected-value estimate has no fixed pattern.
|
|
2306
|
+
const fixedPattern = characteristics?.fixedPatternNoise ? body.fixedPatternNoise.profile : undefined;
|
|
2307
|
+
if (fixedPattern && displayView)
|
|
2308
|
+
fail("simulateDisplayView does not render sensor.fixedPatternNoise; use simulateCapture.");
|
|
2261
2309
|
const channelAt = (x, y) => ["red", "green", "green", "blue"][(y % 2) * 2 + (x % 2)];
|
|
2262
2310
|
function siteInput(index) {
|
|
2263
2311
|
if (!Number.isSafeInteger(index) || index < 0 || index >= nativeWidth * nativeHeight)
|
|
@@ -2285,7 +2333,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2285
2333
|
capacityMeaning: "physical-charge-storage-capacity-electrons", capacityElectrons, scientificStatus: "approximation",
|
|
2286
2334
|
uncertainty: { kind: "not-quantified", limitation: "Derived from the tier's declared saturation signal." }, evidence, siteApplicability: { kind: "exact-site", site },
|
|
2287
2335
|
operatingState: { stateId: IDS.operatingState, evidence }, temperatureApplicability: { kind: "exact-reference-temperature", temperatureC: OPERATING_TEMPERATURE_C } },
|
|
2288
|
-
operatingStateId: IDS.operatingState, readoutProfile: r.readoutProfile, regimeId: r.regimeId } };
|
|
2336
|
+
operatingStateId: IDS.operatingState, readoutProfile: r.readoutProfile, regimeId: r.regimeId, ...(fixedPattern ? { fixedPatternNoise: fixedPattern } : {}) } };
|
|
2289
2337
|
}
|
|
2290
2338
|
const readTile = async (request) => ({ ...request, sites: Array.from({ length: request.width }, (_, j) => siteInput(request.y * nativeWidth + request.x + j)) });
|
|
2291
2339
|
const yieldControl = createHostYield().yieldControl;
|
|
@@ -2302,14 +2350,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2302
2350
|
wavelengthsNm: defocusSiteKernels.wavelengthsNm, kernelRadius: defocusSiteKernels.radius, nativeWidth, nativeHeight, outputWidth: width,
|
|
2303
2351
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { defocusMs += ms; },
|
|
2304
2352
|
...(window ? { window } : {}), ...(bokehZones ? { fieldZones: bokehZones.zones } : {}),
|
|
2305
|
-
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites
|
|
2353
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites } : {}) }) :
|
|
2306
2354
|
diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
2307
2355
|
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
2308
2356
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
2309
2357
|
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
2310
2358
|
...(window ? { window } : {}),
|
|
2311
2359
|
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
2312
|
-
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites
|
|
2360
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites } : {}) }) :
|
|
2361
|
+
// #565: the PSF quadrature reads each tap's source at its lattice point (a unit kernel on #548's lattice), so ties resolve as on the lattice.
|
|
2362
|
+
diffraction && latticeTaps ? createDefocusImageSpaceProvider({ base: provider, geometry: lensGeometry, siteKernels: () => UNIT_SITE_KERNEL,
|
|
2363
|
+
kernelRadius: 0, wavelengthsNm: diffraction.profile.axes.wavelengthNm, nativeWidth, nativeHeight, sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2364
|
+
queryOffsetsSites: [-1, 0, 1].flatMap((k) => queryOffsetsSites.map((o) => ({ x: o.x + k * diffraction.kernelRadiusSamples, y: o.y + k * diffraction.kernelRadiusSamples }))),
|
|
2365
|
+
timesPerBand: temporalSampleCount, ...(window ? { window } : {}) }) : provider;
|
|
2313
2366
|
// Built on first use: a capture that takes the tabulated path (#394 step 2) never queries it, and its kernels cost a setup.
|
|
2314
2367
|
let rawProviderBuilt;
|
|
2315
2368
|
const rawProvider = () => (rawProviderBuilt ??= diffractedProvider(regionNative));
|
|
@@ -2387,16 +2440,30 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2387
2440
|
// site centre plus the kernel radius and the largest query offset.
|
|
2388
2441
|
// #569: a catalogue lens's field-dependent PSF, read per field zone by the lens signal (display-view-lens-signal.ts, loaded on demand).
|
|
2389
2442
|
const viewLensKit = displayView && lensGeometry ? await import("./display-view-lens-signal.js") : undefined;
|
|
2443
|
+
// 2.20.0: the field zones are lens.catalogue's fast method in the view; exact resolves the PSF at every site (zones of one site).
|
|
2390
2444
|
const displayViewLensPsf = viewLensKit && catalogue && lensPsf ?
|
|
2391
|
-
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch) : undefined;
|
|
2445
|
+
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch, catalogueFastRequested ? undefined : Math.max(nativeWidth, nativeHeight)) : undefined;
|
|
2446
|
+
if (displayViewLensPsf && displayViewLensPsf.zoneSites > 1)
|
|
2447
|
+
markFast("lens.catalogue");
|
|
2448
|
+
const viewAhdFast = ahd && resolveFast(demosaicFast, fastTop);
|
|
2449
|
+
if (displayView && viewAhdFast)
|
|
2450
|
+
markFast("output.demosaic.ahd");
|
|
2392
2451
|
if (displayViewLensPsf)
|
|
2393
2452
|
assume("view.lensPsf", "model", { method: "piecewise-constant-field-zones", zoneSites: displayViewLensPsf.zoneSites, kernelRadiusSites: catalogueKernelRadius, scientificStatus: "approximation" }, "Each site takes the lens PSF at its field zone's centre, where the capture resolves it at each query point (docs/DISPLAY_VIEW.md, \"Catalogue lenses\").");
|
|
2394
2453
|
const lensSignal = viewLensKit ? viewLensKit.prepareDisplayViewLensSignal({ geometry: lensGeometry,
|
|
2395
2454
|
times: rotating ? Array.from({ length: temporalSampleCount }, (_, i) => (i + 0.5) * (shutterSeconds / temporalSampleCount)) : [0], distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2396
2455
|
pitch, imagingArea, boxSamples, spots: antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }], psf: displayViewLensPsf, zeroBeyondHorizon: catalogue?.kind === "fisheye" }, nativeWidth, nativeHeight, (diffraction ? diffraction.kernelRadiusSamples : 0) + (displayViewDefocus ? (bokehAxisKernel?.radius ?? defocusDiscKernelRadius(displayViewDefocus.diameterSites)) : 0) + catalogueKernelRadius +
|
|
2397
2456
|
Math.max(0, ...queryOffsetsSites.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))) - 0.5, catalogue ? "lens.catalogue" : imageSpaceOptions.join(", ")) : undefined;
|
|
2457
|
+
// #535: the green channel's expected charge from an 18 % Lambertian grey patch (radiance 0.18·E·S(λ)/π) next to the axis, and the sensor report;
|
|
2458
|
+
// the mid-grey figure only when the request resolves to exact (fast: false, the exports' default): it costs about 0.3 ms.
|
|
2459
|
+
let greyElectrons = fastTop ? undefined : 0;
|
|
2460
|
+
if (!idealReference && greyElectrons !== undefined)
|
|
2461
|
+
for (const [nm, a] of axisChannelResponse(displayView ? "simulateDisplayView" : "simulateCapture").electronsPerRadiance.green)
|
|
2462
|
+
greyElectrons += a * GREY_CARD_REFLECTANCE * irradianceScale * illuminant.shape.evaluate(nm) / Math.PI;
|
|
2463
|
+
const sensor = sensorReport(regimeByChannel.green.readoutProfile, regimeByChannel.green.regimeId, capacityElectrons, greyElectrons, DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, { pixelPitchMicrometers: pitch, peakQuantumEfficiency: 0.5 * (characteristics?.qeScale ?? 1),
|
|
2464
|
+
readoutSeconds: characteristics?.readoutSeconds ?? body.rollingReadoutTiming?.profile.spatialSamplingSkewSeconds?.value ?? null }, fixedPattern);
|
|
2398
2465
|
if (displayView)
|
|
2399
|
-
return await renderView({ lens: lensSignal, lensPsf: displayViewLensPsf, ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, exposureGain, 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 });
|
|
2466
|
+
return await renderView({ sensor, viewFast: { ahd: viewAhdFast, uniformSampling: fastTop }, lens: lensSignal, lensPsf: displayViewLensPsf, ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, exposureGain, 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 });
|
|
2400
2467
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
2401
2468
|
if (idealReference) {
|
|
2402
2469
|
const referenceStarted = clockMs();
|
|
@@ -2577,14 +2644,13 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2577
2644
|
}
|
|
2578
2645
|
let rawOutput;
|
|
2579
2646
|
if (dngParts !== undefined) {
|
|
2580
|
-
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight, partsCaller), providerEvaluationCount: 0 };
|
|
2647
|
+
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight, partsCaller, fastReport.requested), providerEvaluationCount: 0 };
|
|
2581
2648
|
if (run === "develop-from-parts")
|
|
2582
2649
|
assume("parts", "model", { partCount: dngParts.length }, "The whole render's RAW, joined from region renders of this request (#571); how they were rendered is in their results.");
|
|
2583
2650
|
}
|
|
2584
2651
|
else if (tabulated && catalogueFastRun) {
|
|
2585
2652
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2586
|
-
|
|
2587
|
-
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
2653
|
+
markFast("lens.catalogue");
|
|
2588
2654
|
for (const a of lensKit.catalogueFastAssumptions(catalogueFastRun))
|
|
2589
2655
|
assume(a.id, "model", a.value, a.explanation);
|
|
2590
2656
|
}
|
|
@@ -2668,9 +2734,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2668
2734
|
return [channel, { conversionGainElectronsPerCode: resolved.regime.systemConversionGainElectronsPerCode.value / (resolved.regime.adc.digitalGain ?? 1),
|
|
2669
2735
|
readNoiseRmsElectrons: resolved.combinedReadNoiseRmsElectrons }];
|
|
2670
2736
|
}));
|
|
2671
|
-
const dngStarted = clockMs();
|
|
2737
|
+
const dngStarted = clockMs(), opcodes = dngOpcodeSelection && lensKit.createDngLensCorrectionOpcodes(lensKit.resolveGenericLens({ lensId: catalogue.lensId, activeImagingArea: catalogueFrame, fNumber }), dngOpcodeSelection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
2672
2738
|
dng = await serializeNativeRawDng({ raw: rawOutput.raw, colorProfile, noise, ...(dngCapturedAtUtc === undefined ? {} : { capturedAtUtc: dngCapturedAtUtc }),
|
|
2673
|
-
...(textMetadata ? { textMetadata } : {}) });
|
|
2739
|
+
...(textMetadata ? { textMetadata } : {}), ...(opcodes ? { opcodeList3: opcodes.opcodeList3 } : {}) });
|
|
2740
|
+
if (opcodes)
|
|
2741
|
+
assume("output.dng.lensCorrectionOpcodes", "model", { tag: 51022, opcodes: opcodes.opcodes }, "output.lensCorrections' profile as optional DNG opcodes, in order (docs/NATIVE_RAW_AND_PRINT.md, \"Lens-correction opcodes\"); the RAW is unchanged.");
|
|
2674
2742
|
encodeMs = clockMs() - dngStarted;
|
|
2675
2743
|
assumeTextMetadata(TEXT_METADATA_LAYOUT.dng);
|
|
2676
2744
|
checkAbort();
|
|
@@ -2880,6 +2948,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2880
2948
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2881
2949
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" :
|
|
2882
2950
|
defocusImageSpace ? "image-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
2951
|
+
sensor,
|
|
2883
2952
|
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
|
|
2884
2953
|
metering, meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2885
2954
|
standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, resolution },
|