@photivra/engine 2.21.0 → 2.22.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 +118 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +11 -0
- package/dist/capture/c2pa-embed.d.ts +116 -0
- package/dist/capture/c2pa-embed.js +493 -0
- package/dist/capture/c2pa-embed.js.map +1 -0
- package/dist/capture/dng-import.d.ts +73 -8
- package/dist/capture/dng-import.js +315 -80
- package/dist/capture/dng-import.js.map +1 -1
- package/dist/capture/dng-opcodes.d.ts +96 -0
- package/dist/capture/dng-opcodes.js +391 -0
- package/dist/capture/dng-opcodes.js.map +1 -0
- package/dist/capture/dng-profile-maps.d.ts +99 -0
- package/dist/capture/dng-profile-maps.js +381 -0
- package/dist/capture/dng-profile-maps.js.map +1 -0
- package/dist/capture/dng-stage-image.d.ts +23 -0
- package/dist/capture/dng-stage-image.js +65 -0
- package/dist/capture/dng-stage-image.js.map +1 -0
- package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
- package/dist/capture/jpeg-xl-tiles.js +151 -0
- package/dist/capture/jpeg-xl-tiles.js.map +1 -0
- package/dist/capture/lossy-jpeg.d.ts +17 -0
- package/dist/capture/lossy-jpeg.js +369 -0
- package/dist/capture/lossy-jpeg.js.map +1 -0
- package/dist/capture/native-raw-development.d.ts +2 -1
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
- package/dist/color/neighbor-highlight-reconstruction.js +234 -0
- package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
- package/dist/color/sensor-color-development.d.ts +5 -0
- package/dist/color/sensor-color-development.js +7 -1
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/color/spectral-spikiness.d.ts +11 -0
- package/dist/color/spectral-spikiness.js +23 -0
- package/dist/color/spectral-spikiness.js.map +1 -0
- package/dist/core/execution-error.d.ts +21 -0
- package/dist/core/execution-error.js +12 -0
- package/dist/core/execution-error.js.map +1 -0
- 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/catalogue-lens-signal.d.ts +26 -1
- package/dist/facade/catalogue-lens-signal.js +25 -7
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +1 -1
- package/dist/facade/diffraction-provider.js +4 -4
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +38 -6
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +13 -5
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-normal.d.ts +15 -0
- package/dist/facade/display-view-normal.js +44 -0
- package/dist/facade/display-view-normal.js.map +1 -0
- package/dist/facade/display-view-rendering.d.ts +97 -0
- package/dist/facade/display-view-rendering.js +598 -0
- package/dist/facade/display-view-rendering.js.map +1 -0
- package/dist/facade/display-view.d.ts +37 -65
- package/dist/facade/display-view.js +147 -476
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -2
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +1 -1
- package/dist/facade/fast-methods.js +37 -8
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +1 -1
- package/dist/facade/generic-lens-capture.js +1 -1
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane.js +41 -10
- package/dist/facade/image-plane.js.map +1 -1
- package/dist/facade/on-demand-kits.d.ts +14 -0
- package/dist/facade/on-demand-kits.js +36 -0
- package/dist/facade/on-demand-kits.js.map +1 -0
- package/dist/facade/opt-in-kit.d.ts +9 -0
- package/dist/facade/opt-in-kit.js +11 -0
- package/dist/facade/opt-in-kit.js.map +1 -0
- package/dist/facade/quality-kit.d.ts +21 -0
- package/dist/facade/quality-kit.js +23 -0
- package/dist/facade/quality-kit.js.map +1 -0
- package/dist/facade/quality-paths.d.ts +22 -0
- package/dist/facade/quality-paths.js +44 -0
- package/dist/facade/quality-paths.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +18 -2
- package/dist/facade/scene-radiance.js +6 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +32 -13
- package/dist/facade/simulate-capture.js +162 -69
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +10 -12
- package/dist/facade/simulate-display-view.js +57 -48
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
- package/dist/gpu/display-view-gpu-renderer.js +134 -0
- package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
- package/dist/gpu/display-view-image-material-module.d.ts +3 -2
- package/dist/gpu/display-view-image-material-module.js +25 -3
- package/dist/gpu/display-view-image-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-quality.d.ts +43 -1
- package/dist/gpu/display-view-image-quality.js +256 -11
- package/dist/gpu/display-view-image-quality.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -64
- package/dist/gpu/display-view-quality-gpu.js +60 -249
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +16 -16
- package/dist/gpu/display-view-speed-gpu.js +50 -12
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-accelerator.js +3 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +2 -13
- package/dist/gpu/image-kernel-execution.js +2 -10
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
- package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
- package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
- package/dist/gpu/wgsl-display-view-coupled.js +353 -0
- package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -9
- package/dist/gpu/wgsl-display-view-quality-kernel.js +109 -111
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +1 -41
- package/dist/gpu/wgsl-display-view-speed-kernel.js +71 -8
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/index.d.ts +11 -3
- package/dist/index.js +9 -1
- package/dist/index.js.map +1 -1
- package/dist/output/camera-channel-response.d.ts +50 -0
- package/dist/output/camera-channel-response.js +69 -0
- package/dist/output/camera-channel-response.js.map +1 -0
- package/dist/output/native-capture-sdr.js +5 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/sdr-rendering.d.ts +18 -1
- package/dist/output/sdr-rendering.js +39 -2
- package/dist/output/sdr-rendering.js.map +1 -1
- package/dist/sensor/native-sensor-report.d.ts +10 -0
- package/dist/sensor/native-sensor-report.js +16 -0
- package/dist/sensor/native-sensor-report.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +81 -13
- package/dist/sensor/sensor-characteristics.js +10 -6
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/API_REFERENCE.md +117 -4
- package/docs/QUICK_START.md +37 -8
- package/docs/USAGE.md +104 -22
- package/docs/api/capture-c2pa-embed.md +177 -0
- package/docs/api/capture-dng-import.md +86 -10
- package/docs/api/capture-dng-opcodes.md +93 -0
- package/docs/api/capture-dng-profile-maps.md +141 -0
- package/docs/api/capture-dng-stage-image.md +43 -0
- package/docs/api/capture-jpeg-xl-tiles.md +33 -0
- package/docs/api/capture-lossy-jpeg.md +41 -0
- package/docs/api/capture-native-raw-development.md +2 -1
- package/docs/api/core-execution-error.md +43 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +255 -5
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-display-view.md +11 -15
- package/docs/api/facade-fast-methods.md +38 -9
- package/docs/api/facade-quality-paths.md +24 -0
- package/docs/api/facade-scene-radiance.md +1 -1
- package/docs/api/facade-simulate-capture.md +24 -7
- package/docs/api/facade-simulate-display-view.md +10 -12
- package/docs/api/gpu-image-kernel-execution.md +0 -36
- package/docs/api/output-camera-channel-response.md +55 -0
- package/docs/api/output-sdr-rendering.md +6 -1
- package/docs/api/sensor-sensor-characteristics.md +54 -2
- package/llms.txt +1 -0
- package/package.json +6 -5
|
@@ -35,9 +35,10 @@ import { requireInputObject } from "../core/record-validation.js";
|
|
|
35
35
|
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
|
-
import * as sensorProfileKit from "../equipment/sensor-profile-body.js";
|
|
39
38
|
import { createHostYield } from "./host-yield.js";
|
|
40
39
|
import { fastFormatDefault, resolveFast } from "./fast-resolution.js";
|
|
40
|
+
import { SPECTRAL_SPIKINESS_THRESHOLD, spectralSpikiness } from "../color/spectral-spikiness.js";
|
|
41
|
+
import { nativeSensorReport } from "../sensor/native-sensor-report.js";
|
|
41
42
|
import { SCENE_TO_SENSOR_BRIDGE_SCOPE } from "../optics/scene-to-sensor-irradiance.js";
|
|
42
43
|
import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposure-capabilities.js";
|
|
43
44
|
import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
@@ -54,7 +55,6 @@ import { SENSOR_RAW_PRODUCER_NOISE_MODEL } from "../capture/sensor-raw-producer.
|
|
|
54
55
|
import { calculateNativeRawPlan, nativeRawMaximumProviderEvaluations, NATIVE_RAW_LIMITS } from "../capture/native-raw.js";
|
|
55
56
|
import { renderTabulatedNativeRaw, TABULATED_RELATIVE_TOLERANCE } from "../capture/native-environment-tabulated.js";
|
|
56
57
|
import { createDisplayViewExpectedSignal } from "./expected-signal.js";
|
|
57
|
-
import * as mixedIlluminationKit from "./mixed-illumination.js";
|
|
58
58
|
import { buildExpectedSignalInput } from "./expected-signal-setup.js";
|
|
59
59
|
import { createNativeEnvironmentRawTask, createNativeEnvironmentRawRegionTask } from "../capture/native-environment-raw.js";
|
|
60
60
|
import { createNativeRawDevelopmentTask, NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS } from "../capture/native-raw-development.js";
|
|
@@ -62,7 +62,6 @@ import { parseSdrRenderingProfile } from "../output/sdr-rendering.js";
|
|
|
62
62
|
import { createPrintJpegTask, PRINT_JPEG_LIMITS } from "../output/print-jpeg.js";
|
|
63
63
|
import { checkNativeRawIntegrity, prepareNativeRawDevelopment } from "../capture/native-raw-development.js";
|
|
64
64
|
import { createRawPixelDeveloper, normalizedRawSite, rawSiteSaturated } from "../capture/raw-development-kernel.js";
|
|
65
|
-
import { createAhdDemosaicTask } from "../processing/demosaic.js";
|
|
66
65
|
import { PROCESSING_MAX_PIXELS } from "../processing/processing-task.js";
|
|
67
66
|
import { renderSdrRunInto } from "../output/native-capture-sdr.js";
|
|
68
67
|
import { prepareSdrPixelTone } from "../output/sdr-rendering.js";
|
|
@@ -77,14 +76,9 @@ import { prepareEnvironmentLensFieldMapping } from "../optics/sensor-environment
|
|
|
77
76
|
import { createRollingShutterCaptureExposureSchedule, resolveElectronicRollingShutterExposureSchedule } from "../sensor/readout-exposure-linkage.js";
|
|
78
77
|
import { calculateCaptureExposureWindows } from "../sensor/exposure-window.js";
|
|
79
78
|
import { calculateSensorSpectralQuadrature } from "../sensor/spectral-quadrature.js";
|
|
80
|
-
import { createDiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
81
|
-
import { registerDiffractionPsfKey, siteDiffractionKernel } from "../optics/site-diffraction-kernel.js";
|
|
82
79
|
import { convolveSiteKernels, createDefocusDiscSiteKernel, defocusDiscKernelRadius, defocusDiscLimitMessage, requireDefocusDiscKernelRadius, resolveDefocusDisc } from "../optics/defocus-disc-kernel.js";
|
|
83
|
-
import { calculateDefocusFieldVariation, createDefocusImageSpaceProvider, prepareDefocusImageSpaceGeometry } from "./defocus-image-space-provider.js";
|
|
84
80
|
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
85
81
|
import { DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES, siteKernelRuns } from "../optics/site-kernel-runs.js";
|
|
86
|
-
import { createDefocusBokehSiteKernel, resolveDefocusBokehOptions } from "../optics/defocus-bokeh-kernel.js";
|
|
87
|
-
import { planDefocusBokehZones } from "./defocus-bokeh-zones.js";
|
|
88
82
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
89
83
|
import { IDEAL_LENS_LIMITS } from "../equipment/ideal-lens.js";
|
|
90
84
|
import { calculateChannelElectronsPerRadiance } from "./channel-response.js";
|
|
@@ -96,7 +90,7 @@ import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
|
96
90
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
97
91
|
import { correctDiffractionInPlace, defaultDiffractionCorrectionRadiusPixels, designDiffractionCorrectionFilter, parseDiffractionCorrectionSettings, parseUnsharpMaskSettings, renderSdrWithUnsharpMaskInPlace, DEVELOPMENT_DETAIL_MODEL_VERSION } from "../processing/development-detail.js";
|
|
98
92
|
import { resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
99
|
-
import {
|
|
93
|
+
import { loadedKit, optInKit, qualityKit } from "./on-demand-kits.js";
|
|
100
94
|
import { characterizeReadout, parseSensorCharacteristics, sensorReport, SENSOR_CHARACTERISTIC_KEYS } from "../sensor/sensor-characteristics.js";
|
|
101
95
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
102
96
|
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
@@ -112,7 +106,7 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
112
106
|
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
113
107
|
*/
|
|
114
108
|
export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
115
|
-
"lens.diffraction", "lens.defocus", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.subject", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
109
|
+
"lens.diffraction", "lens.defocus", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "sensor.spectralStepNanometers", "motion.rotation", "motion.subject", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
116
110
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
117
111
|
"output.diffractionCorrection", "output.sharpening", "output.lensCorrections", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
118
112
|
/**
|
|
@@ -120,7 +114,8 @@ export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["len
|
|
|
120
114
|
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
121
115
|
* whether or not it has a fast method yet; one without runs exact and reports `false`.
|
|
122
116
|
*/
|
|
123
|
-
export const SIMULATE_CAPTURE_FAST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.diffraction", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd", "motion.subject"
|
|
117
|
+
export const SIMULATE_CAPTURE_FAST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.diffraction", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd", "motion.subject",
|
|
118
|
+
"sensor.spectralStepNanometers"]);
|
|
124
119
|
export const FACADE_EVIDENCE = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({ sourceOrigin: "photivra",
|
|
125
120
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
126
121
|
const DEFAULTS = /* @__PURE__ */ Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
@@ -250,7 +245,7 @@ function cachedDiffractionProfile(input) {
|
|
|
250
245
|
if (kept)
|
|
251
246
|
diffractionProfileCache.delete(key);
|
|
252
247
|
else
|
|
253
|
-
kept = createDiffractionLimitedPsfProfile(input).value;
|
|
248
|
+
kept = loadedKit(qualityKit).createDiffractionLimitedPsfProfile(input).value;
|
|
254
249
|
diffractionProfileCache.set(key, kept);
|
|
255
250
|
let kernelValues = 0;
|
|
256
251
|
for (const profile of diffractionProfileCache.values())
|
|
@@ -581,8 +576,14 @@ function pngColourEncoding(encoding) {
|
|
|
581
576
|
* clipping, white balance, highlight mode and the camera→XYZ→linear-sRGB matrices are all the same. It
|
|
582
577
|
* stops before tone mapping, gamut clipping and quantization. It reads the RAW without taking ownership.
|
|
583
578
|
*/
|
|
584
|
-
async function developLinearSrgb(input, afterRow, ahd) {
|
|
585
|
-
|
|
579
|
+
async function developLinearSrgb(input, afterRow, ahd, neighbors) {
|
|
580
|
+
if (neighbors)
|
|
581
|
+
return developWithNeighborHighlights(input, afterRow, ahd, neighbors.fast, neighbors.module.reconstructHighlightsFromNeighbors);
|
|
582
|
+
return developLinearSrgbWith(prepareNativeRawDevelopment(input), input, afterRow, ahd);
|
|
583
|
+
}
|
|
584
|
+
/** The body of {@link developLinearSrgb} for a prepared development (its operator may be replaced, #602). */
|
|
585
|
+
async function developLinearSrgbWith(v, input, afterRow, ahd) {
|
|
586
|
+
const plan = v.plan, raw = input.raw, layout = plan.frame.colorSamplingProfile.layout;
|
|
586
587
|
if (layout.kind !== "periodic-mosaic")
|
|
587
588
|
fail("Development requires a periodic CFA.");
|
|
588
589
|
const native = plan.exposure.geometry.nativeRaster, g = plan.exposure.resolvedGeometry, active = g.activeCapture.nativeRect;
|
|
@@ -607,6 +608,21 @@ async function developLinearSrgb(input, afterRow, ahd) {
|
|
|
607
608
|
return { rgb, highlight: { mode: v.highlightMode, saturatedPixelCount: c.saturatedPixelCount, whiteBalanceClippedSampleCount: c.whiteBalanceClippedSampleCount,
|
|
608
609
|
reconstructedPixelCount: c.reconstructedPixelCount } };
|
|
609
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* #602: development with the neighbor highlight reconstruction. The development runs with an operator that stops after the white
|
|
613
|
+
* balance (u = g·c per pixel, unclipped), the whole image is rebuilt (`reconstructHighlightsFromNeighbors`) and the colour step
|
|
614
|
+
* (`developWhiteBalancedInto`) finishes each pixel to unclipped linear sRGB.
|
|
615
|
+
*/
|
|
616
|
+
async function developWithNeighborHighlights(input, afterRow, ahd, fast, reconstructHighlightsFromNeighbors) {
|
|
617
|
+
const v = prepareNativeRawDevelopment(input), real = v.development, [g0, g1, g2] = real.appliedGains;
|
|
618
|
+
const whiteBalanceOnly = { ...real, developInto(c0, c1, c2, out, offset) { out[offset] = c0 * g0; out[offset + 1] = c1 * g1; out[offset + 2] = c2 * g2; } };
|
|
619
|
+
const linear = await developLinearSrgbWith({ ...v, development: whiteBalanceOnly }, input, afterRow, ahd);
|
|
620
|
+
const width = v.raster.pixelWidth, height = v.raster.pixelHeight, rgb = linear.rgb;
|
|
621
|
+
const report = reconstructHighlightsFromNeighbors(rgb, width, height, real.appliedGains, fast);
|
|
622
|
+
for (let i = 0; i < width * height; i++)
|
|
623
|
+
real.developWhiteBalancedInto(rgb[3 * i], rgb[3 * i + 1], rgb[3 * i + 2], rgb, 3 * i);
|
|
624
|
+
return { rgb, highlight: { ...linear.highlight, mode: "reconstruct-neighbors", whiteBalanceClippedSampleCount: 0, reconstructedPixelCount: report.reconstructedPixelCount } };
|
|
625
|
+
}
|
|
610
626
|
/**
|
|
611
627
|
* AHD development (2.1): the same site normalization as the native development kernel (flagged or white-level sites
|
|
612
628
|
* are 1, others (code − black)/(white − black)) on the whole native raster, demosaiced by `createAhdDemosaicTask`
|
|
@@ -630,7 +646,7 @@ async function developAhd(v, raw, repeatWidth, repeatHeight, width, height, at,
|
|
|
630
646
|
const m = v.color.normalizedCameraChannelsToXyz;
|
|
631
647
|
const cameraToXyz = [0, 1, 2].map((r) => [0, 1, 2].map((c) => m[r][c] * gains[c]));
|
|
632
648
|
// A caller-raised pixel limit already bounds the raster, so its working memory is not capped again at the 1 GiB default.
|
|
633
|
-
const task = createAhdDemosaicTask({ plane: { width: W, height: H, captureModeId: v.plan.frame.modeId, colorSamplingProfile: v.plan.frame.colorSamplingProfile, samples },
|
|
649
|
+
const task = (await qualityKit.load()).createAhdDemosaicTask({ plane: { width: W, height: H, captureModeId: v.plan.frame.modeId, colorSamplingProfile: v.plan.frame.colorSamplingProfile, samples },
|
|
634
650
|
channelRoles: { red: "red", green: "green", blue: "blue" }, cameraToXyz, ...(v.plan.maximumPixels === undefined ? {} : { maximumWorkingBytes: Number.MAX_SAFE_INTEGER }) }, { yieldControl: async () => { await host.yieldControl(); host.checkAbort(); } });
|
|
635
651
|
const demosaicStarted = clockMs();
|
|
636
652
|
await task.run();
|
|
@@ -966,7 +982,7 @@ viewOption, renderView) {
|
|
|
966
982
|
const profileRecord = (typeof top.sensor === "object" && top.sensor !== null ? top.sensor : {}).profile;
|
|
967
983
|
let profile, profileSensor, profilePitch = DEFAULTS.pixelPitchMicrometers, profileReadNoiseScale = 1;
|
|
968
984
|
if (profileRecord !== undefined) {
|
|
969
|
-
const kit =
|
|
985
|
+
const kit = await optInKit.load();
|
|
970
986
|
profile = kit.parseSensorProfile(profileRecord);
|
|
971
987
|
profilePitch = profile.pixelPitchMicrometers;
|
|
972
988
|
// #554: with a catalogue lens the capture frames on the profile's sensor: by default the largest centred region of the output's
|
|
@@ -1158,6 +1174,8 @@ viewOption, renderView) {
|
|
|
1158
1174
|
diffractionRadiusOption = record.kernelRadiusSamples === undefined ? null : integerIn(record.kernelRadiusSamples, "lens.diffraction.kernelRadiusSamples", 0, 32);
|
|
1159
1175
|
diffractionFast = fastOverride(record, "lens.diffraction");
|
|
1160
1176
|
}
|
|
1177
|
+
// #594: diffraction's code is in the Quality kit, loaded on demand.
|
|
1178
|
+
await qualityKit.load();
|
|
1161
1179
|
if (lensPsf)
|
|
1162
1180
|
fail("lens.diffraction cannot be combined with lens.psf: diffraction would be counted twice.");
|
|
1163
1181
|
if (pupil)
|
|
@@ -1174,7 +1192,7 @@ viewOption, renderView) {
|
|
|
1174
1192
|
const bokeh = { ...record };
|
|
1175
1193
|
delete bokeh.model;
|
|
1176
1194
|
try {
|
|
1177
|
-
defocusBokeh = resolveDefocusBokehOptions(bokeh, "lens.defocus");
|
|
1195
|
+
defocusBokeh = (await qualityKit.load()).resolveDefocusBokehOptions(bokeh, "lens.defocus");
|
|
1178
1196
|
}
|
|
1179
1197
|
catch (error) {
|
|
1180
1198
|
fail(error.message);
|
|
@@ -1214,6 +1232,7 @@ viewOption, renderView) {
|
|
|
1214
1232
|
region: r && { cx: finite(r.centerXM, "region.centerXM"), cy: finite(r.centerYM, "region.centerYM"), hw: positive(r.widthM, "motion.subject.region.widthM") / 2,
|
|
1215
1233
|
hh: positive(r.heightM, "motion.subject.region.heightM") / 2 } };
|
|
1216
1234
|
subjectFast = fastOverride(record, "motion.subject");
|
|
1235
|
+
await optInKit.load();
|
|
1217
1236
|
}
|
|
1218
1237
|
if (subject && pupil)
|
|
1219
1238
|
fail("motion.subject cannot be combined with lens.pupilSampling.");
|
|
@@ -1239,7 +1258,7 @@ viewOption, renderView) {
|
|
|
1239
1258
|
// through one linear map (still camera, rectilinear, no distortion or field throughput); otherwise per tap.
|
|
1240
1259
|
const diffractionObjectSpace = diffractionRadiusOption !== undefined && !rotating && !subject && !lensFieldMapping && !vignetting;
|
|
1241
1260
|
// Output and sensor
|
|
1242
|
-
const outputOptions = allowKeys(top.output, "output", ["width", "height", "format", "toneCurve", "toneCurveParameters", "highlightMode", "exposureCompensationStops", "whiteBalance", "jpegQuantizationStep",
|
|
1261
|
+
const outputOptions = allowKeys(top.output, "output", ["width", "height", "format", "toneCurve", "toneCurveParameters", "highlightMode", "gamutHandling", "exposureCompensationStops", "whiteBalance", "jpegQuantizationStep",
|
|
1243
1262
|
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening", "lensCorrections"]);
|
|
1244
1263
|
// Opt-in large images: a caller-raised native pixel limit. Without it the 4094-pixel sides are unchanged.
|
|
1245
1264
|
const raisedPixels = outputOptions.maximumPixels === undefined ? undefined : integerIn(outputOptions.maximumPixels, "output.maximumPixels", 1, NATIVE_RAW_LIMITS.ceilingPixels);
|
|
@@ -1326,6 +1345,19 @@ viewOption, renderView) {
|
|
|
1326
1345
|
"A site pushed to or past white clips at white and counts as saturated (highlightMode \"reconstruct\" rolls it off). The RAW codes are unchanged" + (format === "dng" ? ", and the DNG holds the unmodified RAW." : "."));
|
|
1327
1346
|
}
|
|
1328
1347
|
const exposureGain = exposureCompensationStops === 0 ? 1 : 2 ** exposureCompensationStops;
|
|
1348
|
+
// #601: the hue-preserving gamut handling. Opt-in: no assumption when omitted.
|
|
1349
|
+
const gamutHandling = outputOptions.gamutHandling ?? "clip-components";
|
|
1350
|
+
if (gamutHandling !== "clip-components" && gamutHandling !== "desaturate-to-luminance")
|
|
1351
|
+
fail("output.gamutHandling must be clip-components or desaturate-to-luminance.");
|
|
1352
|
+
if (outputOptions.gamutHandling !== undefined) {
|
|
1353
|
+
if (linearPath)
|
|
1354
|
+
fail("output.gamutHandling applies to the sRGB rendering only, not with a wide-gamut output.colorEncoding or output.gamutMapping \"perceptual\".");
|
|
1355
|
+
assume("output.gamutHandling", "model", { method: gamutHandling, luminance: "linear-srgb-d65-relative-luminance" }, gamutHandling === "clip-components" ?
|
|
1356
|
+
"Out-of-gamut components are clipped to [0, 1] after the tone curve." :
|
|
1357
|
+
"After the tone curve, a pixel whose largest component exceeds 1 is moved toward the gray of its own luminance until that component is 1: its luminance and hue " +
|
|
1358
|
+
"(the line from the white point through its chromaticity) are kept instead of the component clip's shift toward the secondaries; a pixel whose luminance is at or above 1 is white. " +
|
|
1359
|
+
"Negative components are still clipped to 0. An engine rendering choice, not a camera maker's gamut mapping.");
|
|
1360
|
+
}
|
|
1329
1361
|
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).");
|
|
1330
1362
|
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;
|
|
1331
1363
|
if (format !== "jpeg" && outputOptions.jpegQuantizationStep !== undefined)
|
|
@@ -1335,7 +1367,30 @@ viewOption, renderView) {
|
|
|
1335
1367
|
". 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");
|
|
1336
1368
|
if (pitch < 0.5 || pitch > SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS)
|
|
1337
1369
|
fail("sensor.pixelPitchMicrometers must be 0.5-" + SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS + ".");
|
|
1338
|
-
|
|
1370
|
+
// #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
|
|
1371
|
+
// sRGB samples and the display view, exact for the exports (DNG, JPEG, PNG) and the ideal reference. Fast methods report true when they run.
|
|
1372
|
+
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1373
|
+
fail("fast must be true or false.");
|
|
1374
|
+
const fastTop = resolveFast(top.fast, fastFormatDefault(format, displayView ? "display-view" : idealReference ? "ideal-reference" : "capture"));
|
|
1375
|
+
// #590 (2.22.0): the default step follows the light and the request's quality. A light is spiky when R(20) = 20²·κ/24 > 0.01 (the
|
|
1376
|
+
// midpoint rule's relative error indicator, color/spectral-spikiness.ts) for any of its sources; an exact request then takes 5 nm,
|
|
1377
|
+
// a fast one 20 nm. Its light is validated with the scene below (a light that fails here is not spiky here).
|
|
1378
|
+
const illumination = top.illumination, lights = illumination ? [illumination.sources, illumination.points]
|
|
1379
|
+
.flatMap((list) => (Array.isArray(list) ? list.map((s) => s?.illuminant) : [])) : [top.illuminant];
|
|
1380
|
+
let kappa = 0;
|
|
1381
|
+
for (const light of lights)
|
|
1382
|
+
if (light !== undefined)
|
|
1383
|
+
try {
|
|
1384
|
+
const shape = resolveCaptureIlluminant(light).shape;
|
|
1385
|
+
kappa = Math.max(kappa, spectralSpikiness((nm) => shape.evaluate(nm)));
|
|
1386
|
+
}
|
|
1387
|
+
catch { /* validated below */ }
|
|
1388
|
+
const r20 = 400 * kappa / 24, spiky = r20 > SPECTRAL_SPIKINESS_THRESHOLD, defaultStep = fastTop || !spiky ? DEFAULTS.spectralStepNanometers : 5;
|
|
1389
|
+
const spectralStep = positive(pick(sensorOptions, "spectralStepNanometers", "sensor.spectralStepNanometers", defaultStep, "Spectral midpoint quadrature over 380-780 nm with subintervals of at most " + defaultStep + " nm" + (spiky ? " (the light is spiky: sensor.spectralSpikiness)." : ".")), "sensor.spectralStepNanometers");
|
|
1390
|
+
if (spiky && sensorOptions.spectralStepNanometers === undefined)
|
|
1391
|
+
assume("sensor.spectralSpikiness", "model", { kappaPerSquareNanometer: kappa, r20, threshold: SPECTRAL_SPIKINESS_THRESHOLD, stepNanometers: defaultStep, fast: fastTop }, "#590: the light's spectrum has narrow bands. The midpoint rule's relative error indicator R(h) = h²·κ/24 (DLMF §3.5(i); κ = Σ|Δ²S| / ΣS on a 1 nm grid) is " + r20 +
|
|
1392
|
+
" at 20 nm, above " + SPECTRAL_SPIKINESS_THRESHOLD + " (a design threshold), so an exact request takes 5 nm. " + (fastTop ? "This fast request keeps 20 nm (4× fewer spectral queries): colors may carry a cast (measured up to " +
|
|
1393
|
+
"about 0.1 Δu′v′ on white under the tri-phosphor fluorescent source). Pass fast: false or sensor.spectralStepNanometers: 5 for the exact colors." : "The step costs 4× the spectral queries of 20 nm."));
|
|
1339
1394
|
if (spectralStep < 1 || spectralStep > 100)
|
|
1340
1395
|
fail("sensor.spectralStepNanometers must be 1-100.");
|
|
1341
1396
|
let temporalSampleCount = integerIn(rotating || subject ? pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", ROTATING_TEMPORAL_SAMPLE_COUNT, rotating ? "Eight temporal midpoints across each site's exposure window sample the camera rotation (motion blur); raise it for long exposures or fast rotation." :
|
|
@@ -1387,10 +1442,7 @@ viewOption, renderView) {
|
|
|
1387
1442
|
unsupported("output.region", "pass the rectangle as view.region, in display pixels.");
|
|
1388
1443
|
if (outputOptions.whiteBalance === "auto")
|
|
1389
1444
|
unsupported("output.whiteBalance \"auto\"", "the capture measures it from its noisy full-resolution RAW; use \"scene-illuminant\" or a fixed white balance.");
|
|
1390
|
-
|
|
1391
|
-
unsupported("output.toneCurve " + String(outputOptions.toneCurve), "luminance tone curves couple the channels; the view supports identity and positive-reinhard-per-channel.");
|
|
1392
|
-
if (outputOptions.highlightMode === "reconstruct")
|
|
1393
|
-
unsupported("output.highlightMode \"reconstruct\"", "the view supports the per-channel highlight clip.");
|
|
1445
|
+
// #593 (2.22.0): luminance tone curves, highlightMode "reconstruct" and (#601) the desaturation render (display-view-rendering.ts).
|
|
1394
1446
|
if (outputOptions.diffractionCorrection !== undefined || outputOptions.sharpening !== undefined)
|
|
1395
1447
|
unsupported("output.diffractionCorrection or output.sharpening", "the development detail stages filter the whole developed capture image.");
|
|
1396
1448
|
if (outputOptions.lensCorrections !== undefined)
|
|
@@ -1418,15 +1470,12 @@ viewOption, renderView) {
|
|
|
1418
1470
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
1419
1471
|
fail("output.demosaic must be bilinear or ahd.");
|
|
1420
1472
|
const ahd = demosaicOption === "ahd";
|
|
1421
|
-
// #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
|
|
1422
|
-
// sRGB samples and the display view, exact for the exports (DNG, JPEG, PNG) and the ideal reference. Fast methods report true when they run.
|
|
1423
|
-
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1424
|
-
fail("fast must be true or false.");
|
|
1425
|
-
const fastTop = resolveFast(top.fast, fastFormatDefault(format, displayView ? "display-view" : idealReference ? "ideal-reference" : "capture"));
|
|
1426
1473
|
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
1427
1474
|
["lens.catalogue", catalogue !== undefined, catalogueFast],
|
|
1428
1475
|
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast],
|
|
1429
|
-
["motion.subject", subject !== undefined, subjectFast]
|
|
1476
|
+
["motion.subject", subject !== undefined, subjectFast],
|
|
1477
|
+
// #590: the default step under a spiky light (an explicit step, or a smooth light, makes no choice and adds no key).
|
|
1478
|
+
["sensor.spectralStepNanometers", spiky && sensorOptions.spectralStepNanometers === undefined, undefined]];
|
|
1430
1479
|
const fastApplied = {};
|
|
1431
1480
|
for (const [option, used] of fastInUse)
|
|
1432
1481
|
if (used)
|
|
@@ -1441,6 +1490,9 @@ viewOption, renderView) {
|
|
|
1441
1490
|
fastApplied[option] = true;
|
|
1442
1491
|
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
1443
1492
|
};
|
|
1493
|
+
// #590: the fast method (20 nm under a spiky light) ran.
|
|
1494
|
+
if (spiky && sensorOptions.spectralStepNanometers === undefined && fastTop)
|
|
1495
|
+
markFast("sensor.spectralStepNanometers");
|
|
1444
1496
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
1445
1497
|
fail("output.borderHandling must be margin or mirror.");
|
|
1446
1498
|
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
@@ -1589,7 +1641,7 @@ viewOption, renderView) {
|
|
|
1589
1641
|
// #550: mixed lighting (loaded only when asked for). Its frame-center light stands in for the single illuminant below: the strongest
|
|
1590
1642
|
// source at the center (white balance, the illumination profile) and the center's illuminance (grey card, standard exposure).
|
|
1591
1643
|
// A static import (2.21.0 size pass): as a lazy chunk it made the bundler split the core modules into a shared chunk of their own.
|
|
1592
|
-
const mixedKit = top.illumination === undefined ? undefined :
|
|
1644
|
+
const mixedKit = top.illumination === undefined ? undefined : await optInKit.load();
|
|
1593
1645
|
if (mixedKit && (top.illuminant !== undefined || top.illuminanceLux !== undefined))
|
|
1594
1646
|
fail("illumination replaces illuminant and illuminanceLux; omit them.");
|
|
1595
1647
|
const lightKit = (resolveIlluminant) => mixedKit.resolveMixedIllumination(top.illumination, resolveIlluminant);
|
|
@@ -1649,12 +1701,12 @@ viewOption, renderView) {
|
|
|
1649
1701
|
sceneId: IDS.scene, providerProfileId: IDS.provider, illuminant: sceneIlluminant, irradianceScale: scale, geometry: { distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
1650
1702
|
...(chartSize ? { chart: { widthM: chartSize.widthM, heightM: chartSize.heightM, surroundReflectance: chartSize.surroundReflectance } } : {}) }, evidence: FACADE_EVIDENCE,
|
|
1651
1703
|
limitations: Object.freeze(["Planar Lambertian scene under " + (light ? "blended" : "uniform") + " illumination; no shadows, inter-reflection, gloss or fluorescence."]),
|
|
1652
|
-
...(catalogue?.kind === "fisheye" ? { zeroBeyondHorizon: true } : {}), ...(light ? { irradianceAt: light.irradianceAt } : {}) });
|
|
1704
|
+
...(catalogue?.kind === "fisheye" ? { zeroBeyondHorizon: true } : {}), ...(light ? { irradianceAt: light.irradianceAt, illumination: light } : {}) });
|
|
1653
1705
|
const provider = buildProvider(illuminant, irradianceScale, mixed);
|
|
1654
1706
|
// #551: the moving subject's scene; renderers read it at each query's time. The revealed background is a neutral 18 % reflectance
|
|
1655
1707
|
// under the plane's irradiance at the revealed point: the illuminant, or (#550) the blend and point sources there, from the spectrum.
|
|
1656
1708
|
const half = chartSize ? chartSize.widthM / 2 / metresPerFrameUnit : 1;
|
|
1657
|
-
const movingScene = subject ? (base, scale, shape, light) => createSubjectMotionProvider(base, subject, subjectDistanceM, metresPerFrameUnit, (u, v, nm) => 0.18 * (light ? light.irradianceAt(u / half, v / half, u * metresPerFrameUnit, v * metresPerFrameUnit, nm) : scale * shape(nm)) / Math.PI) : undefined;
|
|
1709
|
+
const movingScene = subject ? (base, scale, shape, light) => loadedKit(optInKit).createSubjectMotionProvider(base, subject, subjectDistanceM, metresPerFrameUnit, (u, v, nm) => 0.18 * (light ? light.irradianceAt(u / half, v / half, u * metresPerFrameUnit, v * metresPerFrameUnit, nm) : scale * shape(nm)) / Math.PI) : undefined;
|
|
1658
1710
|
const sceneProvider = movingScene ? movingScene(provider, irradianceScale, (nm) => illuminant.shape.evaluate(nm), mixed) : provider;
|
|
1659
1711
|
// 2.6: a chart preset's evaluator is engine-built end to end (its results: the schema's exact keys, constant fields,
|
|
1660
1712
|
// the request's identifiers, deep-frozen FACADE_EVIDENCE, UNCERTAINTY and limitations, and an engine-computed
|
|
@@ -1859,7 +1911,7 @@ viewOption, renderView) {
|
|
|
1859
1911
|
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
1860
1912
|
// #551: motion.subject's fast method: with the default count and a still camera, one midpoint per site of the streak, plus one.
|
|
1861
1913
|
if (subject) {
|
|
1862
|
-
const streakSites = subjectTravelM(subject, shutterSeconds, metresPerFrameUnit * Math.hypot(1, aspect)) * width / (2 * metresPerFrameUnit), fewer = Math.ceil(streakSites) + 1;
|
|
1914
|
+
const streakSites = loadedKit(optInKit).subjectTravelM(subject, shutterSeconds, metresPerFrameUnit * Math.hypot(1, aspect)) * width / (2 * metresPerFrameUnit), fewer = Math.ceil(streakSites) + 1;
|
|
1863
1915
|
if (sensorOptions.temporalSampleCount === undefined && !rotating && resolveFast(subjectFast, fastTop) && fewer < temporalSampleCount) {
|
|
1864
1916
|
queriesPerSite = queriesPerSite / temporalSampleCount * fewer;
|
|
1865
1917
|
temporalSampleCount = fewer;
|
|
@@ -1986,7 +2038,11 @@ viewOption, renderView) {
|
|
|
1986
2038
|
// lens.catalogue).
|
|
1987
2039
|
const rollingTaps = !displayView && (rotating || subject !== undefined) && shutterKind === "rolling";
|
|
1988
2040
|
const rollingFastTaps = rollingTaps && (diffractionRadiusOption !== undefined && resolveFast(diffractionFast, fastTop) || catalogueTaps !== undefined && catalogueFastRequested);
|
|
1989
|
-
|
|
2041
|
+
// #594: the image-space provider (lens projection, distortion, vignetting, rotation, a moving subject) is in the Quality kit.
|
|
2042
|
+
const lensGeometryNeeded = (defocusModel !== undefined || displayView || diffractionRadiusOption !== undefined || catalogueTaps !== undefined && !rollingFastTaps) && imageSpaceOptions.length > 0;
|
|
2043
|
+
if (lensGeometryNeeded)
|
|
2044
|
+
await qualityKit.load();
|
|
2045
|
+
const lensGeometry = lensGeometryNeeded ? loadedKit(qualityKit).prepareDefocusImageSpaceGeometry({ focalLengthMm, focusDistanceM,
|
|
1990
2046
|
angularVelocityRadPerSec: angular, ...(lensFieldMapping ? { lensMapping: prepareEnvironmentLensFieldMapping(lensFieldMapping) } : {}), ...(vignetting ? { vignetting } : {}) }) : undefined;
|
|
1991
2047
|
const defocusGeometry = defocusModel !== undefined ? lensGeometry : undefined;
|
|
1992
2048
|
if (rollingFastTaps) {
|
|
@@ -2002,7 +2058,7 @@ viewOption, renderView) {
|
|
|
2002
2058
|
const options = defocusBokeh, side = subjectDistanceM > focusDistanceM ? 1 : -1;
|
|
2003
2059
|
const build = (field) => {
|
|
2004
2060
|
try {
|
|
2005
|
-
return createDefocusBokehSiteKernel({ diameterSites: disc.diameterSites, side, fNumber, focalLengthMm, pitchMicrometers: pitch, options, ...(field ? { field } : {}) });
|
|
2061
|
+
return loadedKit(qualityKit).createDefocusBokehSiteKernel({ diameterSites: disc.diameterSites, side, fNumber, focalLengthMm, pitchMicrometers: pitch, options, ...(field ? { field } : {}) });
|
|
2006
2062
|
}
|
|
2007
2063
|
catch (error) {
|
|
2008
2064
|
const message = error.message;
|
|
@@ -2023,7 +2079,7 @@ viewOption, renderView) {
|
|
|
2023
2079
|
const look = defocusGeometry.lookOf((x - nativeWidth / 2) * mmPerSite, (y - nativeHeight / 2) * mmPerSite, 0);
|
|
2024
2080
|
return { x: look.x / look.z, y: look.y / look.z };
|
|
2025
2081
|
} : (x, y) => ({ x: (x - nativeWidth / 2) * g * metresPerFrameUnit / subjectDistanceM, y: -(y - nativeHeight / 2) * g * metresPerFrameUnit / subjectDistanceM });
|
|
2026
|
-
bokehZones = planDefocusBokehZones({ options, nativeWidth, nativeHeight, radius: axis.radius, fNumber, focalLengthMm, fieldOf, kernelAt: build });
|
|
2082
|
+
bokehZones = loadedKit(qualityKit).planDefocusBokehZones({ options, nativeWidth, nativeHeight, radius: axis.radius, fNumber, focalLengthMm, fieldOf, kernelAt: build });
|
|
2027
2083
|
if (!(bokehZones.minimumOpenAreaRatio > 0))
|
|
2028
2084
|
fail("lens.defocus.catsEye: the clipping apertures close the pupil in part of the frame; reduce shiftPerTanFieldMm or enlarge radiusMm.");
|
|
2029
2085
|
}
|
|
@@ -2055,7 +2111,7 @@ viewOption, renderView) {
|
|
|
2055
2111
|
"to each temporal midpoint's image on the sensor lattice, and under a rolling shutter every row has its own instants, so each row would need its own image of the rows the disc " +
|
|
2056
2112
|
"reaches (about 2R + 1 rows per row, R the kernel radius). Use motion.shutter \"global\", set lens.focusDistanceM equal to subjectDistanceM, or omit one of the options.");
|
|
2057
2113
|
const temporalMidpointsSeconds = rotating ? Array.from({ length: temporalSampleCount }, (_, i) => (i + 0.5) * (shutterSeconds / temporalSampleCount)) : [];
|
|
2058
|
-
return { options: imageSpaceOptions, fieldVariation: calculateDefocusFieldVariation({ geometry: defocusGeometry, ...(lensFieldMapping ? { lensMapping: prepareEnvironmentLensFieldMapping(lensFieldMapping) } : {}),
|
|
2114
|
+
return { options: imageSpaceOptions, fieldVariation: loadedKit(qualityKit).calculateDefocusFieldVariation({ geometry: defocusGeometry, ...(lensFieldMapping ? { lensMapping: prepareEnvironmentLensFieldMapping(lensFieldMapping) } : {}),
|
|
2059
2115
|
focalLengthMm, fNumber, focusDistanceM, subjectDistanceM, pitchMicrometers: pitch, nativeWidth, nativeHeight, angularVelocityRadPerSec: angular, temporalMidpointsSeconds,
|
|
2060
2116
|
diameterSites: disc.diameterSites, discAt: (distanceM) => resolveDefocusDisc({ focalLengthMm, aperture: fNumber, focusDistanceM, subjectDistanceM: distanceM, pitchMicrometers: pitch }).diameterSites }) };
|
|
2061
2117
|
} } : {}) });
|
|
@@ -2072,27 +2128,40 @@ viewOption, renderView) {
|
|
|
2072
2128
|
// #554: a profile's readout is declared at its own photosite area.
|
|
2073
2129
|
const referenceArea = profile ? profile.pixelPitchMicrometers ** 2 : ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS, areaScale = pitch * pitch / referenceArea;
|
|
2074
2130
|
// #555: a full well replaces the area-scaled base-ISO saturation; the charge-domain transfer scales by the same rule.
|
|
2075
|
-
|
|
2131
|
+
// #595: the charge scale at a pitch p (the capture's, and the native photosite's).
|
|
2132
|
+
const fullWell = characteristics?.fullWell, wellAt = (p) => fullWell && (fullWell.link === "none" ? fullWell.electrons : fullWell.electrons * (p / (fullWell.referencePixelPitchMicrometers ?? 6)) ** 2);
|
|
2133
|
+
const fullWellElectrons = wellAt(pitch), chargeScaleAt = (p) => { const well = wellAt(p); return well === undefined ? p * p / referenceArea : well / body.readout[0].profile.regimes[0].preAdcSaturationElectronEquivalent.value; };
|
|
2076
2134
|
if (fullWellElectrons !== undefined && !(fullWellElectrons >= 100 && fullWellElectrons <= 1e10))
|
|
2077
2135
|
fail("sensor.fullWell must give 100 to 1e10 electrons at the pixel pitch (" + fullWellElectrons + ").");
|
|
2078
|
-
const chargeScale =
|
|
2136
|
+
const chargeScale = chargeScaleAt(pitch);
|
|
2137
|
+
// #599 (2.22.0): a tier's site that sums N = (pitch / 6 µm)² photosites sums their read noise in quadrature (×√N), the body's or
|
|
2138
|
+
// sensor.readNoise's (per photosite), as a sensor profile's does (#554, applied in its body); below one photosite it is kept.
|
|
2139
|
+
const tierReadNoiseScale = profile ? 1 : Math.sqrt(Math.max(1, areaScale));
|
|
2079
2140
|
const regimeByChannel = {};
|
|
2080
2141
|
for (const channel of CHANNELS) {
|
|
2081
2142
|
const asset = body.readout.find((a) => a.profile.channelId === channel) ?? fail("Camera readout lacks channel " + channel + ".");
|
|
2082
2143
|
const chain = resolveGenericIsoSignalChain({ profile: body.signalChain, isoCapability: body.iso, requestedIsoSetting: { kind: "standard", exposureIndex: resolvedIso },
|
|
2083
2144
|
captureModeId: IDS.mode, readoutProfile: asset.profile });
|
|
2084
|
-
regimeByChannel[channel] = { readoutProfile: characterizeReadout(asset.profile, chargeScale, characteristics), regimeId: chain.readout.regime.regimeId };
|
|
2145
|
+
regimeByChannel[channel] = { readoutProfile: characterizeReadout(asset.profile, chargeScale, characteristics, tierReadNoiseScale), regimeId: chain.readout.regime.regimeId };
|
|
2085
2146
|
}
|
|
2086
|
-
const
|
|
2087
|
-
|
|
2088
|
-
|
|
2147
|
+
const capacityOf = (scale) => {
|
|
2148
|
+
const regimes = characterizeReadout(body.readout[0].profile, scale, characteristics).regimes;
|
|
2149
|
+
return Math.max(...regimes.map((r) => r.preAdcSaturationElectronEquivalent.value), regimes[0].preAdcSaturationElectronEquivalent.value);
|
|
2150
|
+
};
|
|
2151
|
+
const capacityElectrons = capacityOf(chargeScale);
|
|
2152
|
+
// #595: the same readout at the native photosite (a tier's 6 µm reference, a profile's own pitch): the area rule (or the full well's link) at
|
|
2153
|
+
// that pitch, and the read noise of one photosite (a profile's body carries the view's √N, removed here).
|
|
2154
|
+
const nativePitch = profile ? profile.pixelPitchMicrometers : DEFAULTS.pixelPitchMicrometers, nativeScale = chargeScaleAt(nativePitch);
|
|
2155
|
+
// sensor.readNoise is per photosite on a tier and a profile alike (owner decision 2026-11-11): the native report takes it as given.
|
|
2156
|
+
const nativeSensor = { profile: characterizeReadout(body.readout.find((a) => a.profile.channelId === "green").profile, nativeScale, characteristics, 1 / profileReadNoiseScale),
|
|
2157
|
+
capacityElectrons: capacityOf(nativeScale), pixelPitchMicrometers: nativePitch, framePhotosites: width * height * pitch * pitch / (nativePitch * nativePitch) };
|
|
2089
2158
|
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.");
|
|
2090
2159
|
if (areaScale !== 1 && !fullWell) {
|
|
2091
2160
|
assume("sensor.isoAreaNormalization", "model", { referenceApertureAreaSquareMicrometers: referenceArea,
|
|
2092
2161
|
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, " +
|
|
2093
2162
|
"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 " +
|
|
2094
2163
|
"pre-ADC saturation of every ISO regime are multiplied by area / 36 µm² = " + areaScale + ". The same exposure gives the same output code at any pitch; per-pixel shot-noise SNR " +
|
|
2095
|
-
"falls with smaller pixels. Read noise
|
|
2164
|
+
"falls with smaller pixels. Read noise is the 6 µm photosite's ×√max(1, area / 36 µm²) = " + tierReadNoiseScale + " (#599: the photosites a site sums); dark current stays per pixel. The charge capacity follows the scaled base-ISO saturation, so it is proportional to " +
|
|
2096
2165
|
"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 " +
|
|
2097
2166
|
"proportionally with area across technologies).");
|
|
2098
2167
|
}
|
|
@@ -2110,8 +2179,7 @@ viewOption, renderView) {
|
|
|
2110
2179
|
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 +
|
|
2111
2180
|
" mm frame: a downsized view of the whole frame, with the framing, crop factor, generic prime and minimum focus of that frame. Its signal is the charge of the whole square " +
|
|
2112
2181
|
"(sensor.isoAreaNormalization), so its shot noise is that of all the real photosites in the square summed or averaged (Poisson counts add), and the image brightness is the " +
|
|
2113
|
-
"same as at any other pitch.
|
|
2114
|
-
"sensor's binned output. This is the noise of a binned, downsized view, not the real sensor's per-pixel noise. A point pixel aperture samples one point of each square; " +
|
|
2182
|
+
"same as at any other pitch. Its read noise is the photosites' summed (×√N, #599), its dark current once per output pixel; result.sensor.native has a photosite's figures. This is the noise of a binned, downsized view, not the real sensor's per-pixel noise. A point pixel aperture samples one point of each square; " +
|
|
2115
2183
|
"use sensor.pixelAperture box (or sensor.antiAliasing) to average scene detail finer than the pitch.");
|
|
2116
2184
|
}
|
|
2117
2185
|
assume("sensor.darkCurrent", "model", { electronsPerSecond: DARK_CURRENT_ELECTRONS_PER_SECOND, temperatureC: OPERATING_TEMPERATURE_C }, "Synthetic uniform dark current of 1 e⁻/s per site at 20 °C (the tier presets declare none); dark shot noise follows from it.");
|
|
@@ -2213,16 +2281,29 @@ viewOption, renderView) {
|
|
|
2213
2281
|
const intentOf = (state) => (state ? createCaptureWhiteBalanceIntent({ state }) : null);
|
|
2214
2282
|
const rendering = { schemaVersion: "0.1.0", profileId: "facade-sdr", profileVersion: "1", renderingExposureEv: 0, toneCurve,
|
|
2215
2283
|
...(outputOptions.toneCurveParameters === undefined ? {} : { toneCurveParameters: outputOptions.toneCurveParameters }),
|
|
2216
|
-
gamutHandling
|
|
2284
|
+
gamutHandling, outputDynamicRange: "sdr", transferFunction: "srgb", bitDepth: format === "srgb-16" || (format === "png" && !wideGamut && pngBitDepth === 16) ? 16 : 8, rounding: "nearest-ties-up", dither: "none" };
|
|
2217
2285
|
if (!linearPath)
|
|
2218
|
-
assume("output.rendering", "model", { renderingExposureEv: 0, gamutHandling
|
|
2286
|
+
assume("output.rendering", "model", { renderingExposureEv: 0, gamutHandling }, exposureGain === 1 ? "Developed linear sRGB/D65 with the RAW white level mapped to display white, no exposure push, out-of-gamut components clipped." :
|
|
2219
2287
|
"Developed linear sRGB/D65 with the RAW white level mapped to display white, no further exposure push after development (output.exposureCompensationStops acts on the RAW), out-of-gamut components clipped.");
|
|
2220
2288
|
else
|
|
2221
2289
|
assume("output.rendering", "model", { renderingExposureEv: 0, gamutHandling: "clip-components-in-target-gamut", sdrRendererUsed: false }, "The sRGB SDR renderer is not used: the linear development (RAW white level = relative 1.0, " + (exposureGain === 1 ? "no exposure push" : "after the development exposure push on the RAW") + ") is converted to the target encoding; request.development.rendering is validated but only its exposure and tone curve (identity) apply.");
|
|
2222
2290
|
// Rendering choices are validated before any radiance work, so a bad tone curve fails fast.
|
|
2223
2291
|
parseSdrRenderingProfile(rendering);
|
|
2224
|
-
if (highlightMode !== "clip" && highlightMode !== "reconstruct")
|
|
2225
|
-
fail("output.highlightMode must be clip or reconstruct.");
|
|
2292
|
+
if (highlightMode !== "clip" && highlightMode !== "reconstruct" && highlightMode !== "reconstruct-neighbors")
|
|
2293
|
+
fail("output.highlightMode must be clip, reconstruct or reconstruct-neighbors.");
|
|
2294
|
+
// #602: the neighbor reconstruction reads whole clipped regions, so a region of the frame cannot render it alone.
|
|
2295
|
+
const neighborHighlights = highlightMode === "reconstruct-neighbors", developmentHighlightMode = neighborHighlights ? "clip" : highlightMode;
|
|
2296
|
+
if (neighborHighlights && outputOptions.region !== undefined)
|
|
2297
|
+
fail("output.highlightMode \"reconstruct-neighbors\" reads whole clipped regions; it is not available with output.region.");
|
|
2298
|
+
const neighborFast = fastTop;
|
|
2299
|
+
// #602: loaded on demand (the Quality kit, #594), only for a request that asks for it.
|
|
2300
|
+
const neighborModule = neighborHighlights ? (await qualityKit.load()).neighborHighlights : null;
|
|
2301
|
+
const { NEIGHBOR_HIGHLIGHT_RING, NEIGHBOR_HIGHLIGHT_THRESHOLD } = neighborModule ?? { NEIGHBOR_HIGHLIGHT_RING: 0, NEIGHBOR_HIGHLIGHT_THRESHOLD: 0 };
|
|
2302
|
+
if (neighborHighlights && !displayView)
|
|
2303
|
+
assume("output.highlightMode.reconstructNeighbors", "model", { method: neighborFast ? "ring-average" : "harmonic-interpolation", threshold: NEIGHBOR_HIGHLIGHT_THRESHOLD,
|
|
2304
|
+
ringPixels: NEIGHBOR_HIGHLIGHT_RING, fast: neighborFast, scientificStatus: "estimate" }, "Clipped highlights (a channel at " + NEIGHBOR_HIGHLIGHT_THRESHOLD + " of white or above) are rebuilt from the color ratios of the unclipped pixels around each clipped region: " +
|
|
2305
|
+
(neighborFast ? "the mean ratios of the unclipped pixels within " + NEIGHBOR_HIGHLIGHT_RING + " pixels of the region (fast)" : "their harmonic (Laplace) interpolation inward from the region's unclipped neighbors (full)") +
|
|
2306
|
+
"; each clipped channel becomes the larger of its recorded value and the ratio times the scale of the pixel's unclipped channels. An estimate of the clipped signal, not a recovery; the values are not clipped at white before the tone curve.");
|
|
2226
2307
|
// Frame profiles
|
|
2227
2308
|
const evidence = FACADE_EVIDENCE;
|
|
2228
2309
|
const nativeRaster = { pixelWidth: nativeWidth, pixelHeight: nativeHeight };
|
|
@@ -2357,7 +2438,7 @@ viewOption, renderView) {
|
|
|
2357
2438
|
apertureFNumber: fNumber, signedDefocusImagePlaneMicrometers: 0 }, psfWavelengthBasis: { value: "vacuum", evidence },
|
|
2358
2439
|
spatialModel: { kind: "destination-local-shift-invariant-approximation", evidence, limitation: "Diffraction-limited Airy PSF, the same at every field point." } } };
|
|
2359
2440
|
// Requests with the same profile input and PSF fields share their site diffraction kernels (optics/site-diffraction-kernel.ts).
|
|
2360
|
-
registerDiffractionPsfKey(diffractionPsf.configuration.psf, profileInput);
|
|
2441
|
+
loadedKit(qualityKit).registerDiffractionPsfKey(diffractionPsf.configuration.psf, profileInput);
|
|
2361
2442
|
assume("lens.diffraction", "model", { model: diffraction.model, aperture: "circular-aperture", fNumber, workingFNumber: diffraction.workingFNumber,
|
|
2362
2443
|
kernelRadiusSamples, samplePitchMicrometers: pitch, maximumTruncatedEnergyFraction: diffraction.maximumTruncatedEnergyFraction,
|
|
2363
2444
|
truncatedEnergyByWavelength: diffraction.wavelengths.map((w) => ({ wavelengthNm: w.wavelengthNm, truncatedEnergyFraction: w.truncatedEnergyFraction })),
|
|
@@ -2375,6 +2456,8 @@ viewOption, renderView) {
|
|
|
2375
2456
|
// with the disc, radius R_diffraction + R_defocus (built on first use, kept for the request). Object space, or image space (#548).
|
|
2376
2457
|
let defocusSiteKernels;
|
|
2377
2458
|
if (defocusKernel && defocusKernel.radius > DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES) {
|
|
2459
|
+
// #594: the run lattice of a disc above the direct radius is in the Quality kit.
|
|
2460
|
+
await qualityKit.load();
|
|
2378
2461
|
// #564: above the direct radius the disc is convolved by runs (site-kernel-runs.ts) and each node's diffraction kernel applied
|
|
2379
2462
|
// after it, instead of one combined kernel per node: the same sums regrouped. `at` is never read on this path.
|
|
2380
2463
|
const runs = siteKernelRuns(defocusKernel.weights, defocusKernel.radius), diffractionRadius = diffraction ? diffraction.kernelRadiusSamples : 0;
|
|
@@ -2386,7 +2469,7 @@ viewOption, renderView) {
|
|
|
2386
2469
|
if (!kernel) {
|
|
2387
2470
|
if (!profileWavelengths.includes(wavelengthNm))
|
|
2388
2471
|
fail("simulateCapture: a spectral node has no diffraction kernel.");
|
|
2389
|
-
kernel = siteDiffractionKernel(psfConfiguration, wavelengthNm, pitch, diffractionRadius, "simulateCapture: diffraction kernels must sample the site pitch exactly.");
|
|
2472
|
+
kernel = loadedKit(qualityKit).siteDiffractionKernel(psfConfiguration, wavelengthNm, pitch, diffractionRadius, "simulateCapture: diffraction kernels must sample the site pitch exactly.");
|
|
2390
2473
|
diffractionKernels.set(wavelengthNm, kernel);
|
|
2391
2474
|
}
|
|
2392
2475
|
return kernel;
|
|
@@ -2405,7 +2488,7 @@ viewOption, renderView) {
|
|
|
2405
2488
|
if (!kernel) {
|
|
2406
2489
|
if (!profileWavelengths.includes(wavelengthNm))
|
|
2407
2490
|
fail("simulateCapture: a spectral node has no diffraction kernel.");
|
|
2408
|
-
kernel = convolveSiteKernels(siteDiffractionKernel(psfConfiguration, wavelengthNm, pitch, diffractionRadius, "simulateCapture: diffraction kernels must sample the site pitch exactly."), diffractionRadius, defocusKernel.weights, defocusKernel.radius);
|
|
2491
|
+
kernel = convolveSiteKernels(loadedKit(qualityKit).siteDiffractionKernel(psfConfiguration, wavelengthNm, pitch, diffractionRadius, "simulateCapture: diffraction kernels must sample the site pitch exactly."), diffractionRadius, defocusKernel.weights, defocusKernel.radius);
|
|
2409
2492
|
combined.set(wavelengthNm, kernel);
|
|
2410
2493
|
}
|
|
2411
2494
|
return kernel;
|
|
@@ -2480,7 +2563,7 @@ viewOption, renderView) {
|
|
|
2480
2563
|
let diffractionMs = 0, defocusMs = 0, demosaicMs, encodeMs;
|
|
2481
2564
|
const diffractedProvider = (window) =>
|
|
2482
2565
|
// #548: the disc (or diffraction ⊛ disc) in image space, on the sensor's site lattice, through the mapping, field throughput and rotation.
|
|
2483
|
-
defocusSiteKernels && defocusGeometry && defocusImageSpace ? createDefocusImageSpaceProvider({ base: sceneProvider, geometry: defocusGeometry, siteKernels: defocusSiteKernels.at,
|
|
2566
|
+
defocusSiteKernels && defocusGeometry && defocusImageSpace ? loadedKit(qualityKit).createDefocusImageSpaceProvider({ base: sceneProvider, geometry: defocusGeometry, siteKernels: defocusSiteKernels.at,
|
|
2484
2567
|
...(defocusSiteKernels.large ? { largeKernel: defocusSiteKernels.large } : {}),
|
|
2485
2568
|
wavelengthsNm: defocusSiteKernels.wavelengthsNm, kernelRadius: defocusSiteKernels.radius, nativeWidth, nativeHeight, sitePitchMicrometers: pitch, distanceM: subjectDistanceM,
|
|
2486
2569
|
metresPerFrameUnit, queryOffsetsSites, timesPerBand: temporalSampleCount, onConvolution: (ms) => { defocusMs += ms; }, ...(window ? { window } : {}), ...(subject ? { timeDependentScene: true } : {}),
|
|
@@ -2498,7 +2581,7 @@ viewOption, renderView) {
|
|
|
2498
2581
|
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
2499
2582
|
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites } : {}) }) :
|
|
2500
2583
|
// #565, #588: 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.
|
|
2501
|
-
(diffraction || catalogueTaps !== undefined) && latticeTaps ? createDefocusImageSpaceProvider({ base: sceneProvider, geometry: lensGeometry, siteKernels: () => UNIT_SITE_KERNEL,
|
|
2584
|
+
(diffraction || catalogueTaps !== undefined) && latticeTaps ? loadedKit(qualityKit).createDefocusImageSpaceProvider({ base: sceneProvider, geometry: lensGeometry, siteKernels: () => UNIT_SITE_KERNEL,
|
|
2502
2585
|
kernelRadius: 0, wavelengthsNm: diffraction ? diffraction.profile.axes.wavelengthNm : quadratureWavelengths(), nativeWidth, nativeHeight, sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2503
2586
|
queryOffsetsSites: [-1, 0, 1].flatMap((k) => { const r = k * (diffraction ? diffraction.kernelRadiusSamples : catalogueKernelRadius); return queryOffsetsSites.map((o) => ({ x: o.x + r, y: o.y + r })); }),
|
|
2504
2587
|
timesPerBand: temporalSampleCount, zeroBeyondHorizon: catalogue?.kind === "fisheye", ...(window ? { window } : {}),
|
|
@@ -2585,9 +2668,6 @@ viewOption, renderView) {
|
|
|
2585
2668
|
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch, catalogueFastRequested ? undefined : Math.max(nativeWidth, nativeHeight)) : undefined;
|
|
2586
2669
|
if (displayViewLensPsf && displayViewLensPsf.zoneSites > 1)
|
|
2587
2670
|
markFast("lens.catalogue");
|
|
2588
|
-
const viewAhdFast = ahd && resolveFast(demosaicFast, fastTop);
|
|
2589
|
-
if (displayView && viewAhdFast)
|
|
2590
|
-
markFast("output.demosaic.ahd");
|
|
2591
2671
|
if (displayViewLensPsf)
|
|
2592
2672
|
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\").");
|
|
2593
2673
|
const lensSignal = viewLensKit ? viewLensKit.prepareDisplayViewLensSignal({ geometry: lensGeometry,
|
|
@@ -2603,8 +2683,10 @@ viewOption, renderView) {
|
|
|
2603
2683
|
a * GREY_CARD_REFLECTANCE * irradianceScale * illuminant.shape.evaluate(nm) / Math.PI;
|
|
2604
2684
|
const sensor = sensorReport(regimeByChannel.green.readoutProfile, regimeByChannel.green.regimeId, capacityElectrons, greyElectrons, DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, { pixelPitchMicrometers: pitch, peakQuantumEfficiency: 0.5 * qeScale,
|
|
2605
2685
|
readoutSeconds: characteristics?.readoutSeconds ?? body.rollingReadoutTiming?.profile.spatialSamplingSkewSeconds?.value ?? null }, fixedPattern);
|
|
2686
|
+
// #595: the native photosite's figures (their own module, so a bundler can split them off the default path).
|
|
2687
|
+
sensor.native = nativeSensorReport(sensor, regimeByChannel.green.regimeId, greyElectrons, DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, nativeSensor, fixedPattern);
|
|
2606
2688
|
if (displayView)
|
|
2607
|
-
return await renderView({ sensor, viewFast: {
|
|
2689
|
+
return await renderView({ sensor, viewFast: { uniformSampling: fastTop, rendering: fastTop }, gamutHandling, lens: lensSignal, lensPsf: displayViewLensPsf, movingScene, 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,
|
|
2608
2690
|
mixedIllumination: mixedKit && lightKit });
|
|
2609
2691
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
2610
2692
|
if (idealReference) {
|
|
@@ -2624,7 +2706,9 @@ viewOption, renderView) {
|
|
|
2624
2706
|
// 4. Pixels: the native sites of the output (or region) pixels, each centre-sampled or box-integrated like the capture.
|
|
2625
2707
|
const x0 = (region ? region.x : 0) + margin, y0 = (region ? region.y : 0) + margin, n = boxSamples, unit = 2 / width;
|
|
2626
2708
|
const referenceIntent = intentOf(whiteBalanceState), referenceGains = referenceIntent ? [referenceIntent.channelGains.red, referenceIntent.channelGains.green, referenceIntent.channelGains.blue] : null;
|
|
2627
|
-
const
|
|
2709
|
+
const realDevelopment = prepareSensorColorDevelopment(CAMERA_TO_XYZ, referenceGains, developmentHighlightMode), [rg0, rg1, rg2] = realDevelopment.appliedGains;
|
|
2710
|
+
// #602: the neighbor reconstruction develops to the white balance first, rebuilds the whole image, then finishes the colour.
|
|
2711
|
+
const development = neighborHighlights ? { ...realDevelopment, developInto(c0, c1, c2, out, offset) { out[offset] = c0 * rg0; out[offset + 1] = c1 * rg1; out[offset + 2] = c2 * rg2; } } : realDevelopment;
|
|
2628
2712
|
const rgb = new Float64Array(outputWidth * outputHeight * 3), counts = { whiteBalanceClippedSampleCount: 0, reconstructedPixelCount: 0 }, sum = [0, 0, 0];
|
|
2629
2713
|
// A sample that reads the previous sample's material reuses that sample's integral (nothing is kept per material).
|
|
2630
2714
|
let last = -1, s = [];
|
|
@@ -2654,6 +2738,11 @@ viewOption, renderView) {
|
|
|
2654
2738
|
checkAbort();
|
|
2655
2739
|
}
|
|
2656
2740
|
}
|
|
2741
|
+
if (neighborHighlights) {
|
|
2742
|
+
neighborModule.reconstructHighlightsFromNeighbors(rgb, outputWidth, outputHeight, realDevelopment.appliedGains, neighborFast);
|
|
2743
|
+
for (let i = 0; i < outputWidth * outputHeight; i++)
|
|
2744
|
+
realDevelopment.developWhiteBalancedInto(rgb[3 * i], rgb[3 * i + 1], rgb[3 * i + 2], rgb, 3 * i);
|
|
2745
|
+
}
|
|
2657
2746
|
let samples;
|
|
2658
2747
|
if (!linearPath) {
|
|
2659
2748
|
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
@@ -2732,7 +2821,7 @@ viewOption, renderView) {
|
|
|
2732
2821
|
if (dngParts !== undefined && regionNative)
|
|
2733
2822
|
fail(partsCaller + " takes the whole frame: the options take no output.region.");
|
|
2734
2823
|
let tabulated;
|
|
2735
|
-
// #568: lens.catalogue's fast method:
|
|
2824
|
+
// #568: lens.catalogue's fast method: expected electrons integrated per site at use, with the PSF blended over a field grid (catalogue-lens-signal.ts), for a
|
|
2736
2825
|
// still camera, site-centre queries and a chart or material scene; verified per CFA phase against the exact chain within its tolerance.
|
|
2737
2826
|
let catalogueFastRun;
|
|
2738
2827
|
const catalogueFastEligible = catalogueFastRequested && catalogueTaps !== undefined && lensKit !== undefined && lensPsf !== undefined && dngParts === undefined && tabulatedScene &&
|
|
@@ -2753,7 +2842,10 @@ viewOption, renderView) {
|
|
|
2753
2842
|
const { signalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(1), boxSamples: 1, antiAliasing: undefined, pitch,
|
|
2754
2843
|
materials, radianceAt: provider.radianceAt, imagingArea, focalLengthMm, focusDistanceM, subjectDistanceM, metresPerFrameUnit, caller: "simulateCapture" });
|
|
2755
2844
|
const radial = distortion.profile;
|
|
2756
|
-
|
|
2845
|
+
// #603: the grid follows the request's quality: 2.20.0's faster grid on a fast request (but for the measured lenses that keep the
|
|
2846
|
+
// full one), the full one on an exact request.
|
|
2847
|
+
const gridSubdivisions = lensKit.catalogueFastGridSubdivisions(catalogue.lensId, fastTop);
|
|
2848
|
+
const signal = lensKit.createCatalogueLensSignal({ nativeWidth, nativeHeight, pitch, psf: lensPsf.configuration.psf, wavelengthsNm: wavelengths, electronsPerRadiance: perNode, gridSubdivisions,
|
|
2757
2849
|
scene: signalInput.scene, channelAt: signalInput.channelAt,
|
|
2758
2850
|
distortion: { normalizationRadiusMm: radial.normalizationRadiusMm, coefficients: { k1: radial.coefficients.k1, k2: radial.coefficients.k2 ?? 0, k3: radial.coefficients.k3 ?? 0 } },
|
|
2759
2851
|
...(vignetting ? { vignetting } : {}), projection: projection === "fisheye-equisolid-angle" ? projection : "rectilinear", outsideImageCircleSeesNothing: catalogue.kind === "fisheye",
|
|
@@ -2761,7 +2853,7 @@ viewOption, renderView) {
|
|
|
2761
2853
|
tabulated = await renderTabulatedNativeRaw({ input: environmentRaw, region: regionNative, siteInput, evaluateRadiance: rawProvider().evaluateRadiance,
|
|
2762
2854
|
electrons: signal, relativeTolerance: lensKit.CATALOGUE_FAST_VERIFICATION_TOLERANCE, onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
2763
2855
|
if (tabulated)
|
|
2764
|
-
catalogueFastRun = { ...signal.grid, verificationRelativeDifference: tabulated.verificationRelativeDifference };
|
|
2856
|
+
catalogueFastRun = { ...signal.grid, verificationRelativeDifference: tabulated.verificationRelativeDifference, subdivisions: gridSubdivisions };
|
|
2765
2857
|
}
|
|
2766
2858
|
// #553: a cat's eye varies the kernel over the field, which the expected-signal tables (one kernel per node) do not take.
|
|
2767
2859
|
else if (dngParts === undefined && tabulatedScene && !pupil && !lensFieldMapping && !vignetting && !lensPsf && !rotating && !subject && !bokehZones && (!diffraction || diffractionObjectSpace) &&
|
|
@@ -2807,7 +2899,8 @@ viewOption, renderView) {
|
|
|
2807
2899
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2808
2900
|
// The verification's relative difference itself is not reported: it is a last-bits quantity, and captures use the JavaScript
|
|
2809
2901
|
// engine's own Math functions, so it differs between engines while the results are the same.
|
|
2810
|
-
assume("raw.expectedSignal", "model", { method: "
|
|
2902
|
+
assume("raw.expectedSignal", "model", { method: "per-site-integrated-electrons", verificationSites: 4, tolerance: TABULATED_RELATIVE_TOLERANCE }, "Each native site's expected photo electrons are integrated per site when the site is read: the sum over the pixel-aperture nodes, anti-aliasing spots, diffraction taps and spectral nodes " +
|
|
2903
|
+
"of each tap's material spectrum against the site's channel response (the display view's expected signal), " +
|
|
2811
2904
|
"the same quantity the environment chain computes for this still, rectilinear, distortion-free planar scene, formed in another order, so it can differ in the last bits. The sensor stage (dark charge, Poisson draws, " +
|
|
2812
2905
|
"read noise, ADC) is the native one with each site's own seeds. One site of each CFA phase ran the full chain: its readout was reproduced bit for bit and its electrons agreed within the tolerance. " +
|
|
2813
2906
|
"diagnostics.providerEvaluationCount counts those sites' evaluator calls and the table's scene lookups.");
|
|
@@ -2907,7 +3000,7 @@ viewOption, renderView) {
|
|
|
2907
3000
|
return dng;
|
|
2908
3001
|
// Development
|
|
2909
3002
|
const development = { phaseProfiles, colorProfile,
|
|
2910
|
-
whiteBalance: developmentWhiteBalance, rendering, highlightMode, ...(mirrorBorder ? { borderHandling: "mirror" } : {}),
|
|
3003
|
+
whiteBalance: developmentWhiteBalance, rendering, highlightMode: developmentHighlightMode, ...(mirrorBorder ? { borderHandling: "mirror" } : {}),
|
|
2911
3004
|
...(exposureCompensationStops === 0 ? {} : { exposureCompensationStops }),
|
|
2912
3005
|
maximumRetainedPayloadBytes: developmentPlan.outputBytes + outputWidth * outputHeight * 3 * (rendering.bitDepth / 8) };
|
|
2913
3006
|
let developed;
|
|
@@ -3002,8 +3095,8 @@ viewOption, renderView) {
|
|
|
3002
3095
|
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
3003
3096
|
return { raw: rawOutput.raw, integerSamples: encoded.samples, highlight, diagnostics: encoded.diagnostics };
|
|
3004
3097
|
};
|
|
3005
|
-
if (ahd || ((detailRequested || lensCorrection) && !linearPath)) {
|
|
3006
|
-
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"), ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined);
|
|
3098
|
+
if (ahd || neighborHighlights || ((detailRequested || lensCorrection) && !linearPath)) {
|
|
3099
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"), ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined, neighborModule ? { fast: neighborFast, module: neighborModule } : undefined);
|
|
3007
3100
|
const rgb = await correctLinear(linear.rgb);
|
|
3008
3101
|
if (!linearPath) {
|
|
3009
3102
|
let codes, counts;
|
|
@@ -3035,7 +3128,7 @@ viewOption, renderView) {
|
|
|
3035
3128
|
}
|
|
3036
3129
|
else {
|
|
3037
3130
|
// Wide gamut: the native development operator up to linear sRGB/D65 (unclipped), then XYZ → target primaries.
|
|
3038
|
-
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"));
|
|
3131
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"), undefined, neighborModule ? { fast: neighborFast, module: neighborModule } : undefined);
|
|
3039
3132
|
{
|
|
3040
3133
|
developed = encodeLinear(await correctLinear(linear.rgb), linear.highlight);
|
|
3041
3134
|
if (perceptual) {
|
|
@@ -3095,7 +3188,7 @@ viewOption, renderView) {
|
|
|
3095
3188
|
...(temporalSampleCount > 1 ? ["sensor.temporalSampleCount"] : []), ...(rotating ? ["motion.rotation"] : []), ...(subject ? ["motion.subject"] : []),
|
|
3096
3189
|
...(rollingSchedule ? ["motion.shutter.rolling"] : []), ...(mirrorBorder ? ["output.borderHandling.mirror"] : []), ...(region ? ["output.region"] : []),
|
|
3097
3190
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
3098
|
-
...(highlightMode === "reconstruct" ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
3191
|
+
...(highlightMode === "reconstruct" || neighborHighlights ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
3099
3192
|
...(perceptual ? ["output.gamutMapping.perceptual"] : []), ...(diffractionFilter ? ["output.diffractionCorrection"] : []),
|
|
3100
3193
|
...(sharpening ? ["output.sharpening"] : []), ...(lensCorrection ? ["output.lensCorrections"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
3101
3194
|
// #552: the detail stages' contracts, in execution order inside the development.
|