@photivra/engine 2.8.1 → 2.10.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 +8 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/export-jpeg.d.ts +19 -0
- package/dist/capture/export-jpeg.js +190 -108
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/native-environment-lean.js +106 -12
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +13 -0
- package/dist/core/portable-math.js +43 -0
- package/dist/core/portable-math.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/facade/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/diffraction-provider.js +50 -15
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +75 -0
- package/dist/facade/display-view.js +471 -109
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +36 -4
- package/dist/facade/expected-signal.js +331 -43
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -1
- package/dist/facade/scene-radiance.js +4 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +77 -5
- package/dist/facade/simulate-capture.js +232 -40
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +25 -2
- package/dist/facade/simulate-display-view.js +55 -21
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +5 -0
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +19 -17
- package/dist/optics/diffraction-psf-profile.js +49 -18
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/output/print-jpeg.d.ts +28 -1
- package/dist/output/print-jpeg.js +79 -7
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +12 -0
- package/dist/sensor/stochastic-seed.js +25 -0
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/API_REFERENCE.md +23 -1
- package/docs/QUICK_START.md +77 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/capture-export-jpeg.md +10 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +77 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-scene-radiance.md +2 -1
- package/docs/api/facade-simulate-capture.md +88 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/docs/api/optics-diffraction-psf-profile.md +1 -1
- package/docs/api/output-print-jpeg.md +41 -0
- package/package.json +1 -1
|
@@ -72,9 +72,10 @@ import { createRollingShutterCaptureExposureSchedule, resolveElectronicRollingSh
|
|
|
72
72
|
import { calculateCaptureExposureWindows } from "../sensor/exposure-window.js";
|
|
73
73
|
import { calculateSensorSpectralQuadrature } from "../sensor/spectral-quadrature.js";
|
|
74
74
|
import { createDiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
75
|
+
import { registerDiffractionPsfKey } from "../optics/site-diffraction-kernel.js";
|
|
75
76
|
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
76
77
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
77
|
-
import {
|
|
78
|
+
import { calculateChannelElectronsPerRadiance } from "./channel-response.js";
|
|
78
79
|
import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
|
|
79
80
|
import { prepareSensorColorDevelopment } from "../color/sensor-color-development.js";
|
|
80
81
|
import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
@@ -96,6 +97,12 @@ export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling"
|
|
|
96
97
|
"lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
97
98
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
98
99
|
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
100
|
+
/**
|
|
101
|
+
* The options that take the exact-or-fast flag (#395), in the path style of {@link SIMULATE_CAPTURE_COST_OPTIONS}: each accepts
|
|
102
|
+
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
103
|
+
* whether or not it has a fast method yet; one without runs exact and reports `false`.
|
|
104
|
+
*/
|
|
105
|
+
export const SIMULATE_CAPTURE_FAST_OPTIONS = Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
99
106
|
export const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
100
107
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
101
108
|
const DEFAULTS = Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
@@ -145,8 +152,52 @@ const AA_MAXIMUM_OFFSET_SITES = 4;
|
|
|
145
152
|
/** Channel responses kept across calls (`axisChannelResponse`): one entry per distinct sensor, exposure and optics state. */
|
|
146
153
|
const CHANNEL_RESPONSE_CACHE_SIZE = 8;
|
|
147
154
|
const channelResponseCache = new Map();
|
|
155
|
+
/**
|
|
156
|
+
* #449: diffraction profiles kept across calls, by the JSON of their `createDiffractionLimitedPsfProfile` input (the last
|
|
157
|
+
* DIFFRACTION_PROFILE_CACHE_SIZE, most recently used last). Every region part and detail tile of one request builds the same
|
|
158
|
+
* input. The kept record is private: each call gets its own copy, as mutable as a newly built one, so a caller's change
|
|
159
|
+
* never reaches another call. The copy is made of plain records and arrays (plainDataCopy): the kernel set-up structured-clones
|
|
160
|
+
* the profile once per spectral node, and in a CPU profile of the 96 × 64 Quality tile that clone took about twice as long
|
|
161
|
+
* (24 instead of 12 ms per call) on a deep-frozen record or on a structuredClone copy as on a newly built record or this copy.
|
|
162
|
+
*/
|
|
163
|
+
const DIFFRACTION_PROFILE_CACHE_SIZE = 8;
|
|
164
|
+
const diffractionProfileCache = new Map();
|
|
165
|
+
/** A copy of plain data (records, arrays, primitives; typed arrays are sliced), built from ordinary literals. */
|
|
166
|
+
function plainDataCopy(value) {
|
|
167
|
+
if (Array.isArray(value))
|
|
168
|
+
return value.map(plainDataCopy);
|
|
169
|
+
if (ArrayBuffer.isView(value))
|
|
170
|
+
return value.slice();
|
|
171
|
+
if (value === null || typeof value !== "object")
|
|
172
|
+
return value;
|
|
173
|
+
const copy = {};
|
|
174
|
+
for (const key of Object.keys(value))
|
|
175
|
+
copy[key] = plainDataCopy(value[key]);
|
|
176
|
+
return copy;
|
|
177
|
+
}
|
|
178
|
+
/** The profile of `input` (a pure function of the input record): a copy of the kept one when the same input was built before. */
|
|
179
|
+
function cachedDiffractionProfile(input) {
|
|
180
|
+
const key = JSON.stringify(input);
|
|
181
|
+
let kept = diffractionProfileCache.get(key);
|
|
182
|
+
if (kept)
|
|
183
|
+
diffractionProfileCache.delete(key);
|
|
184
|
+
else
|
|
185
|
+
kept = createDiffractionLimitedPsfProfile(input).value;
|
|
186
|
+
diffractionProfileCache.set(key, kept);
|
|
187
|
+
if (diffractionProfileCache.size > DIFFRACTION_PROFILE_CACHE_SIZE)
|
|
188
|
+
diffractionProfileCache.delete(diffractionProfileCache.keys().next().value);
|
|
189
|
+
// The same fields in the same order as the built record, frozen at the top level like it; `model` is the frozen constant.
|
|
190
|
+
return Object.freeze({ ...kept, profile: plainDataCopy(kept.profile), wavelengths: plainDataCopy(kept.wavelengths), quadrature: plainDataCopy(kept.quadrature) });
|
|
191
|
+
}
|
|
148
192
|
/** Default birefringent separation in sites: the two-spot MTF |cos(π·f·d)| then has its first zero at Nyquist, f = 1/(2·pitch). */
|
|
149
193
|
const AA_DEFAULT_SEPARATION_SITES = 1;
|
|
194
|
+
/** #395: the `fast` override of an option record (undefined when absent), which must be a Boolean. */
|
|
195
|
+
function fastOverride(record, path) {
|
|
196
|
+
const value = record.fast;
|
|
197
|
+
if (value !== undefined && typeof value !== "boolean")
|
|
198
|
+
fail(path + ".fast must be true or false.");
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
150
201
|
/** Parses `sensor.antiAliasing` into engine point-splitting components (offsets in µm, +x right, +y down). */
|
|
151
202
|
function parseAntiAliasing(value, pitch) {
|
|
152
203
|
requireInputObject(value, "sensor.antiAliasing");
|
|
@@ -160,18 +211,18 @@ function parseAntiAliasing(value, pitch) {
|
|
|
160
211
|
return d;
|
|
161
212
|
};
|
|
162
213
|
if (kind === "four-spot") {
|
|
163
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
214
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "separationSites", "fast"]), d = separation(record), h = d * pitch / 2;
|
|
164
215
|
return { kind, separationSites: d, components: [[-h, -h], [h, -h], [-h, h], [h, h]].map(([x, y]) => ({ offsetMicrometers: { x: x, y: y }, normalizedWeight: 0.25 })) };
|
|
165
216
|
}
|
|
166
217
|
if (kind === "two-spot") {
|
|
167
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "axis", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
218
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "axis", "separationSites", "fast"]), d = separation(record), h = d * pitch / 2;
|
|
168
219
|
if (record.axis !== "horizontal" && record.axis !== "vertical")
|
|
169
220
|
fail("sensor.antiAliasing.axis must be horizontal or vertical.");
|
|
170
221
|
const axis = record.axis;
|
|
171
222
|
return { kind, axis, separationSites: d, components: [-h, h].map((o) => ({ offsetMicrometers: axis === "horizontal" ? { x: o, y: 0 } : { x: 0, y: o }, normalizedWeight: 0.5 })) };
|
|
172
223
|
}
|
|
173
224
|
if (kind === "point-splitting") {
|
|
174
|
-
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "components"]);
|
|
225
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "components", "fast"]);
|
|
175
226
|
if (!Array.isArray(record.components) || record.components.length === 0 || record.components.length > AA_MAXIMUM_COMPONENTS) {
|
|
176
227
|
fail("sensor.antiAliasing.components must be an array of 1 to " + AA_MAXIMUM_COMPONENTS + " components.");
|
|
177
228
|
}
|
|
@@ -283,11 +334,25 @@ function spectrumDigestForm(raw, prepared) {
|
|
|
283
334
|
values.push(prepared.evaluate(nm));
|
|
284
335
|
return { kind: "function-sampled-1nm", rangeNanometers: [FACADE_SPECTRAL_RANGE_NANOMETERS.minimum, FACADE_SPECTRAL_RANGE_NANOMETERS.maximum], values };
|
|
285
336
|
}
|
|
337
|
+
/**
|
|
338
|
+
* #449: the CIE 1931 ȳ values at luminanceOf's 5 nm nodes, evaluated once (estimateCie1931ColorMatchingFunctions is a pure
|
|
339
|
+
* function of the wavelength, so each entry is the value the loop would compute).
|
|
340
|
+
*/
|
|
341
|
+
let luminanceCmfY;
|
|
342
|
+
function luminanceNodesY() {
|
|
343
|
+
if (luminanceCmfY)
|
|
344
|
+
return luminanceCmfY;
|
|
345
|
+
const values = [];
|
|
346
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5)
|
|
347
|
+
values.push(estimateCie1931ColorMatchingFunctions(nm).y);
|
|
348
|
+
return (luminanceCmfY = Float64Array.from(values));
|
|
349
|
+
}
|
|
286
350
|
function luminanceOf(radiance) {
|
|
351
|
+
const cmfY = luminanceNodesY();
|
|
287
352
|
let sum = 0;
|
|
288
|
-
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5) {
|
|
353
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum, i = 0; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5, i++) {
|
|
289
354
|
const w = nm === FACADE_SPECTRAL_RANGE_NANOMETERS.minimum || nm === FACADE_SPECTRAL_RANGE_NANOMETERS.maximum ? 2.5 : 5;
|
|
290
|
-
sum += w * radiance(nm) *
|
|
355
|
+
sum += w * radiance(nm) * cmfY[i];
|
|
291
356
|
}
|
|
292
357
|
return PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W * sum;
|
|
293
358
|
}
|
|
@@ -409,14 +474,29 @@ async function developAhd(v, raw, repeatWidth, repeatHeight, width, height, at,
|
|
|
409
474
|
host.onDemosaicMs?.(clockMs() - demosaicStarted);
|
|
410
475
|
const demosaiced = task.takeOutput().data, rgb = new Float64Array(width * height * 3);
|
|
411
476
|
const counts = { saturatedPixelCount: 0, whiteBalanceClippedSampleCount: 0, reconstructedPixelCount: 0 };
|
|
477
|
+
// #420: a pixel is clipped when a saturated site lies within AHD_VALUE_REACH of it, clipped to the raster. The sites within
|
|
478
|
+
// that reach of each column are found once per row (`near`, a horizontal dilation of `saturated`), so a pixel reads one
|
|
479
|
+
// column of 2·reach + 1 rows instead of the whole square; with no saturated site at all no pixel is clipped.
|
|
480
|
+
let anySaturated = false;
|
|
481
|
+
for (let k = 0; k < saturated.length && !anySaturated; k++)
|
|
482
|
+
anySaturated = saturated[k] !== 0;
|
|
483
|
+
const near = anySaturated ? new Uint8Array(W * H) : undefined;
|
|
484
|
+
if (near)
|
|
485
|
+
for (let y = 0; y < H; y++) {
|
|
486
|
+
const row = y * W;
|
|
487
|
+
for (let x = 0; x < W; x++)
|
|
488
|
+
if (saturated[row + x] !== 0)
|
|
489
|
+
near.fill(1, row + Math.max(0, x - AHD_VALUE_REACH), row + Math.min(W, x + AHD_VALUE_REACH + 1));
|
|
490
|
+
}
|
|
412
491
|
for (let y = 0; y < height; y++) {
|
|
413
|
-
|
|
414
|
-
|
|
492
|
+
// The oriented-to-native map is affine in x with whole-site steps (as the bilinear path's developRun uses): two points give the row.
|
|
493
|
+
const first = at(0, y), second = width > 1 ? at(1, y) : first, stepX = second.x - first.x, stepY = second.y - first.y;
|
|
494
|
+
for (let x = 0, px = first.x, py = first.y; x < width; x++, px += stepX, py += stepY) {
|
|
495
|
+
const i = py * W + px;
|
|
415
496
|
let clipped = false;
|
|
416
|
-
|
|
417
|
-
for (let
|
|
418
|
-
|
|
419
|
-
if (sx >= 0 && sy >= 0 && sx < W && sy < H && saturated[sy * W + sx] !== 0) {
|
|
497
|
+
if (near)
|
|
498
|
+
for (let sy = Math.max(0, py - AHD_VALUE_REACH), last = Math.min(H - 1, py + AHD_VALUE_REACH); sy <= last; sy++) {
|
|
499
|
+
if (near[sy * W + px] !== 0) {
|
|
420
500
|
clipped = true;
|
|
421
501
|
break;
|
|
422
502
|
}
|
|
@@ -485,6 +565,74 @@ function encodeSdrFromLinear(rgb, encoding, bitDepth, mapping) {
|
|
|
485
565
|
export async function simulateCapture(options) {
|
|
486
566
|
return await runCapture(options, "capture");
|
|
487
567
|
}
|
|
568
|
+
/**
|
|
569
|
+
* Writes the whole-frame DNG of `options` (`output.format: "dng"`, no `output.region`) from the packed RAW of region renders of
|
|
570
|
+
* the same request (#391), so the RAW can render in parallel. A region render's native sites are the same sites as the whole
|
|
571
|
+
* render's (each site's seeds come from its index in the whole raster), so the joined RAW is the whole render's RAW, and the
|
|
572
|
+
* request is resolved and written exactly as `simulateCapture` writes its DNG: the bytes are the same. The parts must cover
|
|
573
|
+
* every native site of the frame; where they overlap (each region carries the demosaic margin) their sites must agree. Region
|
|
574
|
+
* renders of the request with any format serve (a region's own `output.format` does not change its RAW). A region cannot take
|
|
575
|
+
* `output.whiteBalance: "auto"` or `output.dng`: render the parts with a fixed white balance (the RAW codes do not depend on the
|
|
576
|
+
* white-balance intent) and pass the DNG's own options here, where auto white balance is measured over the joined frame.
|
|
577
|
+
* @returns The DNG (its `bytes` are those of `SimulateCaptureResult.image.dng` for the same options), with its stored levels and identifiers.
|
|
578
|
+
*/
|
|
579
|
+
export async function serializeSimulatedCaptureDngFromParts(options, parts) {
|
|
580
|
+
return await runCapture(options, "dng-from-parts", parts);
|
|
581
|
+
}
|
|
582
|
+
/** The whole frame's packed RAW from region parts (see `serializeSimulatedCaptureDngFromParts`). */
|
|
583
|
+
function joinNativeRawParts(value, plan, width, height) {
|
|
584
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
585
|
+
throw new InvalidConfigurationError("serializeSimulatedCaptureDngFromParts needs at least one part.");
|
|
586
|
+
const parts = value, count = width * height;
|
|
587
|
+
const codes = new Uint16Array(count), blackLevels = new Uint16Array(count), digitalSaturationCodes = new Uint16Array(count), saturationFlags = new Uint8Array(count);
|
|
588
|
+
const covered = new Uint8Array(count), white = new Uint16Array(4);
|
|
589
|
+
const bad = (k, what) => { throw new InvalidConfigurationError("DNG part " + k + ": " + what); };
|
|
590
|
+
parts.forEach((part, k) => {
|
|
591
|
+
const rect = part?.region?.nativeRect, raw = part?.raw;
|
|
592
|
+
if (!rect || !raw)
|
|
593
|
+
bad(k, "needs raw and region.nativeRect (a region render's result).");
|
|
594
|
+
const { x, y, width: w, height: h } = rect;
|
|
595
|
+
if (![x, y, w, h].every(Number.isSafeInteger) || x < 0 || y < 0 || w < 1 || h < 1 || x + w > width || y + h > height)
|
|
596
|
+
bad(k, "its nativeRect is not inside the frame's native raster.");
|
|
597
|
+
const n = w * h;
|
|
598
|
+
if (!(raw.codes instanceof Uint16Array) || raw.codes.length !== n || !(raw.blackLevels instanceof Uint16Array) || raw.blackLevels.length !== n ||
|
|
599
|
+
!(raw.digitalSaturationCodes instanceof Uint16Array) || raw.digitalSaturationCodes.length !== n || !(raw.saturationFlags instanceof Uint8Array) || raw.saturationFlags.length !== n)
|
|
600
|
+
bad(k, "its RAW arrays do not match its nativeRect.");
|
|
601
|
+
const phases = raw.effectiveSaturationCodes;
|
|
602
|
+
if (!(phases instanceof Uint16Array) || phases.length !== 4)
|
|
603
|
+
bad(k, "needs four per-phase white levels.");
|
|
604
|
+
// Per-phase white levels are in the whole raster's CFA phases (0 for a phase the part has no site of); all parts agree.
|
|
605
|
+
for (let phase = 0; phase < 4; phase++) {
|
|
606
|
+
const code = phases[phase];
|
|
607
|
+
if (code === 0)
|
|
608
|
+
continue;
|
|
609
|
+
if (white[phase] === 0)
|
|
610
|
+
white[phase] = code;
|
|
611
|
+
else if (white[phase] !== code)
|
|
612
|
+
bad(k, "its white level differs from another part's: not the same request.");
|
|
613
|
+
}
|
|
614
|
+
for (let row = 0; row < h; row++)
|
|
615
|
+
for (let column = 0; column < w; column++) {
|
|
616
|
+
const from = row * w + column, to = (y + row) * width + x + column;
|
|
617
|
+
if (covered[to]) {
|
|
618
|
+
if (codes[to] !== raw.codes[from] || blackLevels[to] !== raw.blackLevels[from] || digitalSaturationCodes[to] !== raw.digitalSaturationCodes[from] ||
|
|
619
|
+
saturationFlags[to] !== raw.saturationFlags[from])
|
|
620
|
+
bad(k, "a site differs from another part's at the same place: not the same request.");
|
|
621
|
+
continue;
|
|
622
|
+
}
|
|
623
|
+
covered[to] = 1;
|
|
624
|
+
codes[to] = raw.codes[from];
|
|
625
|
+
blackLevels[to] = raw.blackLevels[from];
|
|
626
|
+
digitalSaturationCodes[to] = raw.digitalSaturationCodes[from];
|
|
627
|
+
saturationFlags[to] = raw.saturationFlags[from];
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
if (covered.includes(0))
|
|
631
|
+
throw new InvalidConfigurationError("The DNG parts do not cover every native site of the frame.");
|
|
632
|
+
if (white.includes(0))
|
|
633
|
+
throw new InvalidConfigurationError("The DNG parts lack a white level for a CFA phase.");
|
|
634
|
+
return { plan, codes, blackLevels, digitalSaturationCodes, saturationFlags, effectiveSaturationCodes: white };
|
|
635
|
+
}
|
|
488
636
|
/**
|
|
489
637
|
* The ideal reference of a capture request (2.3): the same scene, framing, chart size, output size, region and pixel
|
|
490
638
|
* grid as `simulateCapture(options)` would use, rendered with no camera effects (no lens blur, diffraction, defocus,
|
|
@@ -527,7 +675,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
527
675
|
const started = Date.now(), idealReference = run === "ideal-reference", displayView = run === "display-view";
|
|
528
676
|
requireInputObject(options, "simulateCapture options");
|
|
529
677
|
const top = allowKeys(options, "simulateCapture options", ["camera", "lens", "exposure", "scene", "subjectDistanceM", "chartSize", "illuminant",
|
|
530
|
-
"illuminanceLux", "output", "sensor", "motion", "signal", "onProgress"]);
|
|
678
|
+
"illuminanceLux", "output", "sensor", "motion", "fast", "signal", "onProgress"]);
|
|
531
679
|
const signal = top.signal, onProgress = top.onProgress;
|
|
532
680
|
if (signal !== undefined && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean"))
|
|
533
681
|
fail("signal must be an AbortSignal.");
|
|
@@ -631,16 +779,21 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
631
779
|
lensPsf = p;
|
|
632
780
|
}
|
|
633
781
|
// Diffraction (2.1): validated here, built once the f-number is resolved.
|
|
634
|
-
let diffractionRadiusOption;
|
|
782
|
+
let diffractionRadiusOption, diffractionFast;
|
|
635
783
|
if (lensOptions.diffraction !== undefined) {
|
|
636
784
|
const d = lensOptions.diffraction;
|
|
637
785
|
if (d === "circular-aperture")
|
|
638
786
|
diffractionRadiusOption = null;
|
|
639
787
|
else {
|
|
640
|
-
const record = allowKeys(d, "lens.diffraction", ["aperture", "kernelRadiusSamples"]);
|
|
641
|
-
|
|
642
|
-
|
|
788
|
+
const record = allowKeys(d, "lens.diffraction", ["kind", "aperture", "kernelRadiusSamples", "fast"]);
|
|
789
|
+
// #395: the record names the model by `kind`; `aperture` is its deprecated alias (until 3.0.0).
|
|
790
|
+
if (record.kind !== undefined && record.aperture !== undefined)
|
|
791
|
+
fail("lens.diffraction takes kind or its deprecated alias aperture, not both.");
|
|
792
|
+
const model = record.kind !== undefined ? record.kind : record.aperture;
|
|
793
|
+
if (model !== "circular-aperture")
|
|
794
|
+
fail("lens.diffraction must be \"circular-aperture\" or { kind: \"circular-aperture\", kernelRadiusSamples, fast }.");
|
|
643
795
|
diffractionRadiusOption = record.kernelRadiusSamples === undefined ? null : integerIn(record.kernelRadiusSamples, "lens.diffraction.kernelRadiusSamples", 0, 32);
|
|
796
|
+
diffractionFast = fastOverride(record, "lens.diffraction");
|
|
644
797
|
}
|
|
645
798
|
if (lensPsf)
|
|
646
799
|
fail("lens.diffraction cannot be combined with lens.psf: diffraction would be counted twice.");
|
|
@@ -690,6 +843,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
690
843
|
}
|
|
691
844
|
else if (outputOptions.dng !== undefined)
|
|
692
845
|
fail("output.dng applies only to output.format \"dng\".");
|
|
846
|
+
if (run === "dng-from-parts" && format !== "dng")
|
|
847
|
+
fail("serializeSimulatedCaptureDngFromParts needs output.format \"dng\".");
|
|
693
848
|
const textMetadata = parseOutputTextMetadata(outputOptions.metadata, "output.metadata");
|
|
694
849
|
if (textMetadata && format !== "jpeg" && format !== "png" && format !== "dng")
|
|
695
850
|
fail("output.metadata applies only to output.format \"jpeg\", \"png\" or \"dng\".");
|
|
@@ -739,16 +894,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
739
894
|
pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", DEFAULTS.temporalSampleCount, "One temporal sample: the static scene and still camera make more samples unnecessary."), "sensor.temporalSampleCount", 1, 256);
|
|
740
895
|
const noiseSeed = integerIn(pick(sensorOptions, "noiseSeed", "sensor.noiseSeed", DEFAULTS.noiseSeed, "Fixed noise seed 1 (deterministic output)."), "sensor.noiseSeed", 0, 0xffffffff);
|
|
741
896
|
const antiAliasing = sensorOptions.antiAliasing === undefined ? undefined : parseAntiAliasing(sensorOptions.antiAliasing, pitch);
|
|
897
|
+
const antiAliasingFast = antiAliasing ? fastOverride(sensorOptions.antiAliasing, "sensor.antiAliasing") : undefined;
|
|
742
898
|
// Pixel aperture (2.3): N × N midpoint nodes over the square photosite; 1 = the default centre sample.
|
|
743
|
-
let boxSamples = 1;
|
|
899
|
+
let boxSamples = 1, boxFast;
|
|
744
900
|
if (sensorOptions.pixelAperture !== undefined && sensorOptions.pixelAperture !== "point") {
|
|
745
901
|
const a = sensorOptions.pixelAperture;
|
|
746
902
|
if (a === "box")
|
|
747
903
|
boxSamples = SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES;
|
|
748
904
|
else {
|
|
749
|
-
const record = allowKeys(a, "sensor.pixelAperture", ["kind", "samplesPerAxis"]);
|
|
905
|
+
const record = allowKeys(a, "sensor.pixelAperture", ["kind", "samplesPerAxis", "fast"]);
|
|
750
906
|
if (record.kind !== "box")
|
|
751
|
-
fail("sensor.pixelAperture must be \"point\", \"box\" or { kind: \"box\", samplesPerAxis }.");
|
|
907
|
+
fail("sensor.pixelAperture must be \"point\", \"box\" or { kind: \"box\", samplesPerAxis, fast }.");
|
|
908
|
+
boxFast = fastOverride(record, "sensor.pixelAperture");
|
|
752
909
|
boxSamples = record.samplesPerAxis === undefined ? SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES : integerIn(record.samplesPerAxis, "sensor.pixelAperture.samplesPerAxis", 1, 16);
|
|
753
910
|
}
|
|
754
911
|
}
|
|
@@ -780,10 +937,35 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
780
937
|
layout: { kind: "periodic-mosaic", repeatWidthSites: 2, repeatHeightSites: 2, siteChannelIds: ["red", "green", "green", "blue"], anchor: "native-sensor-top-left-site" } };
|
|
781
938
|
// Demosaic (2.0): built-in Bayer kernels and border handling. Omitted, the 1.0 bilinear kernels and a
|
|
782
939
|
// one-site rendered border are used unchanged.
|
|
783
|
-
|
|
940
|
+
// #395: the record form { kind, fast } names the same demosaic.
|
|
941
|
+
let demosaicOption, demosaicFast;
|
|
942
|
+
if (outputOptions.demosaic !== undefined && typeof outputOptions.demosaic === "object" && outputOptions.demosaic !== null) {
|
|
943
|
+
const record = allowKeys(outputOptions.demosaic, "output.demosaic", ["kind", "fast"]);
|
|
944
|
+
if (record.kind !== "bilinear" && record.kind !== "ahd")
|
|
945
|
+
fail("output.demosaic must be bilinear or ahd, or { kind, fast }.");
|
|
946
|
+
demosaicOption = record.kind;
|
|
947
|
+
demosaicFast = fastOverride(record, "output.demosaic");
|
|
948
|
+
}
|
|
949
|
+
else
|
|
950
|
+
demosaicOption = outputOptions.demosaic;
|
|
951
|
+
const borderOption = outputOptions.borderHandling;
|
|
784
952
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
785
953
|
fail("output.demosaic must be bilinear or ahd.");
|
|
786
954
|
const ahd = demosaicOption === "ahd";
|
|
955
|
+
// #395: exact or fast per option. No option has a fast method yet, so each runs exact and reports false.
|
|
956
|
+
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
957
|
+
fail("fast must be true or false.");
|
|
958
|
+
const fastTop = top.fast === true;
|
|
959
|
+
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
960
|
+
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast]];
|
|
961
|
+
const fastApplied = {};
|
|
962
|
+
for (const [option, used] of fastInUse)
|
|
963
|
+
if (used)
|
|
964
|
+
fastApplied[option] = false;
|
|
965
|
+
const fastReport = Object.freeze({
|
|
966
|
+
requested: fastTop || [diffractionFast, antiAliasingFast, boxFast, demosaicFast].some((value) => value === true),
|
|
967
|
+
applied: Object.freeze(fastApplied), assurance: "engine-reference"
|
|
968
|
+
});
|
|
787
969
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
788
970
|
fail("output.borderHandling must be margin or mirror.");
|
|
789
971
|
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
@@ -1258,13 +1440,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1258
1440
|
// larger tail energy outside it is listed per wavelength like any other (2.2.1 states the cap in the explanation).
|
|
1259
1441
|
const thirdRingSites = Math.max(1, Math.ceil(AIRY_THIRD_ZERO_PER_LAMBDA_N * lambdaN / pitch));
|
|
1260
1442
|
const kernelRadiusSamples = diffractionRadiusOption ?? Math.min(32, thirdRingSites);
|
|
1261
|
-
|
|
1443
|
+
const profileInput = { aperture: "circular", focalLengthMm, apertureFNumber: fNumber, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
1262
1444
|
wavelengthsNm: wavelengths, samplePitchMicrometers: pitch, kernelRadiusSamples, fieldHalfExtentMm: aaReachMm === 0 ? { x: imagingArea.widthMm / 2, y: imagingArea.heightMm / 2 } :
|
|
1263
1445
|
{ x: imagingArea.widthMm / 2 + aaReachMm, y: imagingArea.heightMm / 2 + aaReachMm },
|
|
1264
|
-
profileId: "facade-diffraction-airy" }
|
|
1446
|
+
profileId: "facade-diffraction-airy" };
|
|
1447
|
+
// #449: a copy of the profile kept for this input; it equals createDiffractionLimitedPsfProfile(profileInput).value field by
|
|
1448
|
+
// field, so the registration below (keyed by profileInput) holds for it.
|
|
1449
|
+
diffraction = cachedDiffractionProfile(profileInput);
|
|
1265
1450
|
diffractionPsf = { kind: "sampled-local", configuration: { psf: { profile: diffraction.profile, focalLengthMm, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
1266
1451
|
apertureFNumber: fNumber, signedDefocusImagePlaneMicrometers: 0 }, psfWavelengthBasis: { value: "vacuum", evidence },
|
|
1267
1452
|
spatialModel: { kind: "destination-local-shift-invariant-approximation", evidence, limitation: "Diffraction-limited Airy PSF, the same at every field point." } } };
|
|
1453
|
+
// Requests with the same profile input and PSF fields share their site diffraction kernels (optics/site-diffraction-kernel.ts).
|
|
1454
|
+
registerDiffractionPsfKey(diffractionPsf.configuration.psf, profileInput);
|
|
1268
1455
|
assume("lens.diffraction", "model", { model: diffraction.model, aperture: "circular-aperture", fNumber, workingFNumber: diffraction.workingFNumber,
|
|
1269
1456
|
kernelRadiusSamples, samplePitchMicrometers: pitch, maximumTruncatedEnergyFraction: diffraction.maximumTruncatedEnergyFraction,
|
|
1270
1457
|
truncatedEnergyByWavelength: diffraction.wavelengths.map((w) => ({ wavelengthNm: w.wavelengthNm, truncatedEnergyFraction: w.truncatedEnergyFraction })),
|
|
@@ -1356,9 +1543,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1356
1543
|
delete environment.evaluateApertureRadiance;
|
|
1357
1544
|
environments[channel] = environment;
|
|
1358
1545
|
}
|
|
1359
|
-
// The responses are a function of these records
|
|
1360
|
-
//
|
|
1361
|
-
|
|
1546
|
+
// The responses are a function of these records without their scene bindings: the radiance is replaced at every node,
|
|
1547
|
+
// and the bindings (provider, illumination and material profiles) are only checked, never enter a number. So a chart,
|
|
1548
|
+
// illuminant or illuminance change keeps the entry (#443). A request split into region tiles builds the same records for
|
|
1549
|
+
// every tile. The entries are kept across calls (the last CHANNEL_RESPONSE_CACHE_SIZE).
|
|
1550
|
+
const key = JSON.stringify(CHANNELS.map((c) => ({ ...environments[c], sceneBindings: undefined })));
|
|
1362
1551
|
const copy = (responses) => Object.fromEntries(CHANNELS.map((c) => [c, new Map(responses[c])]));
|
|
1363
1552
|
const cached = channelResponseCache.get(key);
|
|
1364
1553
|
let electronsPerRadiance;
|
|
@@ -1369,15 +1558,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1369
1558
|
}
|
|
1370
1559
|
else {
|
|
1371
1560
|
electronsPerRadiance = {};
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
.photo.value.photoSignal.expectedGeneratedElectronCount;
|
|
1377
|
-
const nodes = new Set();
|
|
1378
|
-
electrons((nm) => { nodes.add(nm); return 1; });
|
|
1379
|
-
electronsPerRadiance[channel] = new Map([...nodes].sort((a, b) => a - b).map((node) => [node, electrons((nm) => (nm === node ? 1 : 0))]));
|
|
1380
|
-
}
|
|
1561
|
+
// One chain execution per channel, each node's electrons read from its own rate term (#443; exact, see channel-response.ts).
|
|
1562
|
+
// The records are built above for this call only, so they are planned as engine-owned data.
|
|
1563
|
+
for (const channel of CHANNELS)
|
|
1564
|
+
electronsPerRadiance[channel] = calculateChannelElectronsPerRadiance(environments[channel], (request) => provider.evaluateRadiance(request), caller);
|
|
1381
1565
|
// Callers get their own maps; the cached ones are never handed out.
|
|
1382
1566
|
channelResponseCache.set(key, copy(electronsPerRadiance));
|
|
1383
1567
|
if (channelResponseCache.size > CHANNEL_RESPONSE_CACHE_SIZE)
|
|
@@ -1400,7 +1584,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1400
1584
|
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1401
1585
|
// that only captures does not contain the display view.
|
|
1402
1586
|
if (displayView)
|
|
1403
|
-
return await renderView({ ahd, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, chartReport, chartSize, diffraction, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1587
|
+
return await renderView({ ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, chartReport, chartSize, diffraction, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
1404
1588
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1405
1589
|
if (idealReference) {
|
|
1406
1590
|
const referenceStarted = clockMs();
|
|
@@ -1516,8 +1700,12 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1516
1700
|
// #394 step 2: an engine-built planar scene (chart preset or caller material scene; still, rectilinear, no distortion, vignetting,
|
|
1517
1701
|
// pupil rays or caller PSF) takes its expected electrons from the tabulated signal, verified per CFA phase against the chain.
|
|
1518
1702
|
const tabulatedScene = chart !== undefined || callerScene?.kind === "material-function";
|
|
1703
|
+
// #391: a DNG from region parts takes the whole frame's RAW from the parts instead of rendering it.
|
|
1704
|
+
if (run === "dng-from-parts" && regionNative)
|
|
1705
|
+
fail("serializeSimulatedCaptureDngFromParts writes the whole frame: the options take no output.region.");
|
|
1706
|
+
const dngParts = run === "dng-from-parts" ? viewOption : undefined;
|
|
1519
1707
|
let tabulated;
|
|
1520
|
-
if (tabulatedScene && !pupil && !lensFieldMapping && !vignetting && !lensPsf && !rotating && (!diffraction || diffractionObjectSpace) && provider.chartMaterialAt && provider.materialRadiance) {
|
|
1708
|
+
if (dngParts === undefined && tabulatedScene && !pupil && !lensFieldMapping && !vignetting && !lensPsf && !rotating && (!diffraction || diffractionObjectSpace) && provider.chartMaterialAt && provider.materialRadiance) {
|
|
1521
1709
|
const { electronsPerRadiance, wavelengths } = axisChannelResponse("simulateCapture");
|
|
1522
1710
|
const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0));
|
|
1523
1711
|
const sceneMaterials = chart ? chart.materials : callerScene.materials, materialRadiance = provider.materialRadiance;
|
|
@@ -1534,7 +1722,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1534
1722
|
electrons: createDisplayViewExpectedSignal(signalInput), onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
1535
1723
|
}
|
|
1536
1724
|
let rawOutput;
|
|
1537
|
-
if (
|
|
1725
|
+
if (dngParts !== undefined)
|
|
1726
|
+
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight), providerEvaluationCount: 0 };
|
|
1727
|
+
else if (tabulated) {
|
|
1538
1728
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
1539
1729
|
// The verification's relative difference itself is not reported: it is a last-bits quantity, and captures use the JavaScript
|
|
1540
1730
|
// engine's own Math functions, so it differs between engines while the results are the same.
|
|
@@ -1582,7 +1772,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1582
1772
|
sampleSet: { imageStateId: "facade-pre-wb-raw", inputDomain: "relative-pre-wb-camera-linear-rgb", samples } });
|
|
1583
1773
|
finalRawInput = { raw: buildRawInput(intentOf(whiteBalanceState)), sceneBinding, maximumProviderEvaluations: environmentRaw.maximumProviderEvaluations };
|
|
1584
1774
|
const wbTick = progress("white-balance", plan.tileCount);
|
|
1585
|
-
if (tabulated) {
|
|
1775
|
+
if (tabulated || dngParts !== undefined) {
|
|
1586
1776
|
// #394: the RAW codes do not depend on the white-balance intent (it is development metadata), so the tabulated RAW is
|
|
1587
1777
|
// kept with the plan of the measured intent instead of being rendered again.
|
|
1588
1778
|
rawOutput = { ...rawOutput, raw: { ...rawOutput.raw, plan: calculateNativeRawPlan(finalRawInput.raw) } };
|
|
@@ -1623,6 +1813,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1623
1813
|
assume("output.dng", "model", { writer: "serializeNativeRawDng", schemaVersion: dng.schemaVersion, whiteLevel: dng.whiteLevel, blackLevels: dng.blackLevels,
|
|
1624
1814
|
noiseProfile: dng.noiseProfile, unrepresentedSaturationFlagCount: dng.unrepresentedSaturationFlagCount }, "Lossless uncompressed 16-bit CFA DNG of exactly the packed native RAW codes: per-phase BlackLevel, WhiteLevel = smallest per-phase effective white, ActiveArea and DefaultCrop from the capture geometry, ColorMatrix1/ForwardMatrix1 from the facade's diagonal camera→XYZ profile (D65), AsShotNeutral from the white-balance intent, BaselineExposure 0, and NoiseProfile from each channel's readout regime (conversion gain ÷ digital gain, combined read noise; Poisson + Gaussian + quantization only). No linear master or developed image is declared; saturation flags below WhiteLevel are not representable and are counted.");
|
|
1625
1815
|
}
|
|
1816
|
+
if (dngParts !== undefined)
|
|
1817
|
+
return dng;
|
|
1626
1818
|
// Development
|
|
1627
1819
|
const development = { phaseProfiles, colorProfile,
|
|
1628
1820
|
whiteBalance: developmentWhiteBalance, rendering, highlightMode, ...(mirrorBorder ? { borderHandling: "mirror" } : {}),
|
|
@@ -1756,7 +1948,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1756
1948
|
timingsMs: { raw: rawMs, whiteBalance: wbMs, development: devMs, jpeg: jpegMs, total: Date.now() - started,
|
|
1757
1949
|
...(diffraction && diffractionObjectSpace ? { diffraction: diffractionMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
1758
1950
|
...(encodeMs === undefined ? {} : { encode: encodeMs }) },
|
|
1759
|
-
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))) },
|
|
1951
|
+
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))), fast: fastReport },
|
|
1760
1952
|
provenance: { scientificStatus: "approximation", cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion,
|
|
1761
1953
|
evidence: [...FACADE_EVIDENCE, ...preset.evidence],
|
|
1762
1954
|
composedContracts: ["resolveGenericEquipmentExposureCapabilities", "meterRelativeExposure", "createExposureMeterTargetFromMeteringResult",
|