@photivra/engine 2.18.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +179 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -10
- package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
- package/dist/capture/dng-lens-correction-opcodes.js +119 -0
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
- package/dist/capture/focus-control.js +2 -75
- package/dist/capture/focus-control.js.map +1 -1
- package/dist/capture/native-environment-lean.d.ts +1 -0
- package/dist/capture/native-environment-lean.js +2 -6
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.d.ts +1 -0
- package/dist/capture/native-environment-optical-stage.js +2 -5
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-environment-raw.js +1 -5
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +6 -0
- package/dist/capture/native-raw-dng.js +16 -2
- package/dist/capture/native-raw-dng.js.map +1 -1
- package/dist/capture/release-sequence.js +2 -47
- package/dist/capture/release-sequence.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +3 -5
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/color/white-balance.d.ts +2 -0
- package/dist/color/white-balance.js +3 -56
- package/dist/color/white-balance.js.map +1 -1
- package/dist/composition/image-formation-plan.d.ts +1 -0
- package/dist/composition/image-formation-plan.js +3 -49
- package/dist/composition/image-formation-plan.js.map +1 -1
- package/dist/core/evidence-provenance.js +1 -16
- package/dist/core/evidence-provenance.js.map +1 -1
- package/dist/core/record-validation.d.ts +54 -0
- package/dist/core/record-validation.js +176 -0
- package/dist/core/record-validation.js.map +1 -1
- package/dist/core/scientific-assurance.js +1 -18
- package/dist/core/scientific-assurance.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +35 -0
- package/dist/core/shared-helpers.js +38 -0
- package/dist/core/shared-helpers.js.map +1 -0
- package/dist/core/spectral-composition.js +1 -36
- package/dist/core/spectral-composition.js.map +1 -1
- package/dist/core/spectral.js +1 -19
- package/dist/core/spectral.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/exposure-capabilities.d.ts +1 -0
- package/dist/equipment/exposure-capabilities.js +2 -36
- package/dist/equipment/exposure-capabilities.js.map +1 -1
- package/dist/equipment/generic-lens-catalogue.d.ts +8 -2
- package/dist/equipment/generic-lens-catalogue.js +31 -2
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.js +2 -3
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.js +2 -3
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/equipment/iso-capabilities.js +2 -49
- package/dist/equipment/iso-capabilities.js.map +1 -1
- package/dist/equipment/metering-capabilities.js +1 -26
- package/dist/equipment/metering-capabilities.js.map +1 -1
- package/dist/equipment/release-capabilities.js +1 -53
- package/dist/equipment/release-capabilities.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.js +1 -7
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
- package/dist/exposure/exposure-duration-control.js +1 -16
- package/dist/exposure/exposure-duration-control.js.map +1 -1
- package/dist/exposure/exposure-mode-resolver.js +1 -17
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/filtered-exposure-time.d.ts +93 -0
- package/dist/exposure/filtered-exposure-time.js +129 -0
- package/dist/exposure/filtered-exposure-time.js.map +1 -0
- package/dist/exposure/flash-ttl.d.ts +1 -0
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -49
- package/dist/exposure/flash.js.map +1 -1
- package/dist/exposure/metering-scene-radiance.js +1 -25
- package/dist/exposure/metering-scene-radiance.js.map +1 -1
- package/dist/exposure/metering-target.js +1 -30
- package/dist/exposure/metering-target.js.map +1 -1
- package/dist/exposure/metering-temporal.js +1 -8
- package/dist/exposure/metering-temporal.js.map +1 -1
- package/dist/exposure/metering.js +1 -72
- package/dist/exposure/metering.js.map +1 -1
- package/dist/exposure/program-line.js +1 -45
- package/dist/exposure/program-line.js.map +1 -1
- package/dist/facade/develop-joined-parts.d.ts +29 -0
- package/dist/facade/develop-joined-parts.js +33 -0
- package/dist/facade/develop-joined-parts.js.map +1 -0
- package/dist/facade/display-view-gpu.d.ts +7 -4
- package/dist/facade/display-view-gpu.js +4 -4
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +1 -1
- package/dist/facade/display-view-lens-signal.js +5 -3
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.d.ts +3 -1
- package/dist/facade/display-view.js +1 -1
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +50 -0
- package/dist/facade/fast-methods.js.map +1 -0
- package/dist/facade/fast-resolution.d.ts +12 -0
- package/dist/facade/fast-resolution.js +18 -0
- package/dist/facade/fast-resolution.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +6 -3
- package/dist/facade/generic-lens-capture.js +6 -3
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +88 -16
- package/dist/facade/simulate-capture.js +279 -153
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +3 -2
- package/dist/facade/simulate-display-view.js +22 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +1 -3
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +1 -5
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -1
- package/dist/gpu/display-view-quality-gpu.js +4 -8
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.js +3 -25
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.js +1 -5
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +2 -0
- package/dist/gpu/webgpu-runtime.js +3 -0
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/index.d.ts +11 -1
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -1
- package/dist/motion/camera-rotation.js +1 -6
- package/dist/motion/camera-rotation.js.map +1 -1
- package/dist/motion/capture-rotation-inverse-mapping.d.ts +12 -0
- package/dist/motion/capture-rotation-inverse-mapping.js +2 -2
- package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-rotation-temporal-quadrature.js +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
- package/dist/motion/capture-rotation-trajectory.js +2 -17
- package/dist/motion/capture-rotation-trajectory.js.map +1 -1
- package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js +3 -14
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
- package/dist/motion/extended-object-projection.js +5 -40
- package/dist/motion/extended-object-projection.js.map +1 -1
- package/dist/optics/aperture-diameter.d.ts +47 -0
- package/dist/optics/aperture-diameter.js +50 -0
- package/dist/optics/aperture-diameter.js.map +1 -0
- package/dist/optics/atmospheric-haze.js +1 -7
- package/dist/optics/atmospheric-haze.js.map +1 -1
- package/dist/optics/brown-conrady-distortion.js +1 -7
- package/dist/optics/brown-conrady-distortion.js.map +1 -1
- package/dist/optics/complex-pupil-psf.d.ts +1 -0
- package/dist/optics/complex-pupil-psf.js +2 -84
- package/dist/optics/complex-pupil-psf.js.map +1 -1
- package/dist/optics/detail-limit.d.ts +182 -0
- package/dist/optics/detail-limit.js +336 -0
- package/dist/optics/detail-limit.js.map +1 -0
- package/dist/optics/focus-state.js +1 -11
- package/dist/optics/focus-state.js.map +1 -1
- package/dist/optics/front-of-lens-filter.d.ts +5 -0
- package/dist/optics/front-of-lens-filter.js +3 -65
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/generic-lens-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +25 -2
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/generic-nd-filter-presets.d.ts +12 -0
- package/dist/optics/generic-nd-filter-presets.js +48 -0
- package/dist/optics/generic-nd-filter-presets.js.map +1 -0
- package/dist/optics/illumination-vignetting.js +1 -6
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +4 -1
- package/dist/optics/lens-psf-profile.js +12 -111
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/longitudinal-chromatic-aberration.js +1 -7
- package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -1
- package/dist/optics/pupil-vignetting.js +1 -7
- package/dist/optics/pupil-vignetting.js.map +1 -1
- package/dist/optics/radial-distortion.js +1 -6
- package/dist/optics/radial-distortion.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +11 -96
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/stray-light.d.ts +1 -0
- package/dist/optics/stray-light.js +1 -1
- package/dist/optics/stray-light.js.map +1 -1
- package/dist/optics/zernike-wavefront.js +1 -7
- package/dist/optics/zernike-wavefront.js.map +1 -1
- package/dist/output/capture-geometry.js +1 -8
- package/dist/output/capture-geometry.js.map +1 -1
- package/dist/output/crop-to-match-focal-length.d.ts +62 -0
- package/dist/output/crop-to-match-focal-length.js +63 -0
- package/dist/output/crop-to-match-focal-length.js.map +1 -0
- package/dist/output/developed-lens-corrections.d.ts +62 -0
- package/dist/output/developed-lens-corrections.js +135 -0
- package/dist/output/developed-lens-corrections.js.map +1 -0
- package/dist/output/generic-lens-corrections.d.ts +5 -7
- package/dist/output/generic-lens-corrections.js +11 -10
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/geometric-transforms.js +1 -4
- package/dist/output/geometric-transforms.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +12 -2
- package/dist/output/lens-corrections.js +22 -7
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/native-capture-sdr.d.ts +2 -2
- package/dist/output/native-capture-sdr.js +3 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/print-contrast-reference.js +1 -1
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-size-blur-noise.d.ts +110 -0
- package/dist/output/print-size-blur-noise.js +138 -0
- package/dist/output/print-size-blur-noise.js.map +1 -0
- package/dist/processing/development-detail.d.ts +29 -0
- package/dist/processing/development-detail.js +128 -107
- package/dist/processing/development-detail.js.map +1 -1
- package/dist/schema/illumination-temporal.js +3 -72
- package/dist/schema/illumination-temporal.js.map +1 -1
- package/dist/schema/illumination.d.ts +1 -0
- package/dist/schema/illumination.js +2 -65
- package/dist/schema/illumination.js.map +1 -1
- package/dist/schema/scene-radiance-temporal.js +2 -14
- package/dist/schema/scene-radiance-temporal.js.map +1 -1
- package/dist/schema/scene-radiance.js +2 -60
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/schema/validation.d.ts +2 -0
- package/dist/schema/validation.js +2 -17
- package/dist/schema/validation.js.map +1 -1
- package/dist/sensor/accumulated-charge.js +2 -59
- package/dist/sensor/accumulated-charge.js.map +1 -1
- package/dist/sensor/architecture.js +1 -16
- package/dist/sensor/architecture.js.map +1 -1
- package/dist/sensor/camera-saturation-capacity.d.ts +1 -0
- package/dist/sensor/camera-saturation-capacity.js +3 -55
- package/dist/sensor/camera-saturation-capacity.js.map +1 -1
- package/dist/sensor/capture-color-sampling-binding.js +1 -26
- package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
- package/dist/sensor/capture-mode-timing.js +1 -39
- package/dist/sensor/capture-mode-timing.js.map +1 -1
- package/dist/sensor/capture-mode.js +1 -23
- package/dist/sensor/capture-mode.js.map +1 -1
- package/dist/sensor/color-sampling.js +1 -26
- package/dist/sensor/color-sampling.js.map +1 -1
- package/dist/sensor/constant-rate-temporal-integration.js +2 -46
- package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
- package/dist/sensor/dark-current.js +2 -45
- package/dist/sensor/dark-current.js.map +1 -1
- package/dist/sensor/eqe-electron-rate.js +2 -41
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +7 -0
- package/dist/sensor/exposure-window.js +9 -24
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/iso-gain.js +1 -7
- package/dist/sensor/iso-gain.js.map +1 -1
- package/dist/sensor/iso-signal-chain.js +1 -43
- package/dist/sensor/iso-signal-chain.js.map +1 -1
- package/dist/sensor/optical-stack.js +1 -25
- package/dist/sensor/optical-stack.js.map +1 -1
- package/dist/sensor/photon-energy.js +1 -26
- package/dist/sensor/photon-energy.js.map +1 -1
- package/dist/sensor/physical-charge-capacity.js +3 -77
- package/dist/sensor/physical-charge-capacity.js.map +1 -1
- package/dist/sensor/radiometry-readiness.js +2 -23
- package/dist/sensor/radiometry-readiness.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +3 -0
- package/dist/sensor/raw-readout.js +4 -66
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/raw-reconstruction.js +2 -64
- package/dist/sensor/raw-reconstruction.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -9
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/dist/sensor/readout-timing.js +4 -79
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.d.ts +2 -0
- package/dist/sensor/response-application-compatibility.js +4 -35
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/response-operating-range.d.ts +1 -0
- package/dist/sensor/response-operating-range.js +3 -79
- package/dist/sensor/response-operating-range.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +4 -114
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sampling-aperture.js +1 -25
- package/dist/sensor/sampling-aperture.js.map +1 -1
- package/dist/sensor/sensor-characteristics.d.ts +137 -0
- package/dist/sensor/sensor-characteristics.js +115 -0
- package/dist/sensor/sensor-characteristics.js.map +1 -0
- package/dist/sensor/spatial-sample-reduction.js +3 -24
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spatial-sampling-quadrature.js +1 -3
- package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
- package/dist/sensor/spatio-spectral-reduction.js +1 -18
- package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
- package/dist/sensor/spectral-coverage.d.ts +10 -1
- package/dist/sensor/spectral-coverage.js +1 -1
- package/dist/sensor/spectral-coverage.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +3 -41
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/spectral-response.js +1 -47
- package/dist/sensor/spectral-response.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +3 -0
- package/dist/sensor/temporal-photo-signal.js +5 -2
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/stabilization/camera-shake.js +1 -6
- package/dist/stabilization/camera-shake.js.map +1 -1
- package/dist/stabilization/system.d.ts +9 -0
- package/dist/stabilization/system.js +7 -86
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +5 -0
- package/docs/API_REFERENCE.md +116 -3
- package/docs/QUICK_START.md +178 -14
- package/docs/USAGE.md +111 -3
- package/docs/api/capture-native-raw-dng.md +6 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +7 -2
- package/docs/api/exports.json +203 -3
- package/docs/api/exposure-filtered-exposure-time.md +105 -0
- package/docs/api/facade-develop-joined-parts.md +32 -0
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-fast-methods.md +91 -0
- package/docs/api/facade-simulate-capture.md +72 -16
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/optics-generic-nd-filter-presets.md +34 -0
- package/docs/api/optics-lens-psf-profile.md +3 -1
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-developed-lens-corrections.md +12 -0
- package/docs/api/output-generic-lens-corrections.md +3 -10
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +5 -0
- package/package.json +1 -1
|
@@ -36,6 +36,8 @@ import { ENGINE_API_VERSION } from "../core/version.js";
|
|
|
36
36
|
import { GENERIC_TIER_ASSETS } from "../equipment/generic-tier-assets.js";
|
|
37
37
|
import { parseCallerCameraPreset } from "./camera-preset.js";
|
|
38
38
|
import { createHostYield } from "./host-yield.js";
|
|
39
|
+
import { fastFormatDefault, resolveFast } from "./fast-resolution.js";
|
|
40
|
+
import { SCENE_TO_SENSOR_BRIDGE_SCOPE } from "../optics/scene-to-sensor-irradiance.js";
|
|
39
41
|
import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposure-capabilities.js";
|
|
40
42
|
import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
41
43
|
import { meterRelativeExposure } from "../exposure/metering.js";
|
|
@@ -90,8 +92,9 @@ import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
|
90
92
|
import { multiplyMatrices } from "../color/colorimetry-math.js";
|
|
91
93
|
import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
92
94
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
93
|
-
import {
|
|
94
|
-
import {
|
|
95
|
+
import { correctDiffractionInPlace, defaultDiffractionCorrectionRadiusPixels, designDiffractionCorrectionFilter, parseDiffractionCorrectionSettings, parseUnsharpMaskSettings, renderSdrWithUnsharpMaskInPlace, DEVELOPMENT_DETAIL_MODEL_VERSION } from "../processing/development-detail.js";
|
|
96
|
+
import { resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
97
|
+
import { characterizeReadout, parseSensorCharacteristics, sensorReport, SENSOR_CHARACTERISTIC_KEYS } from "../sensor/sensor-characteristics.js";
|
|
95
98
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
96
99
|
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
97
100
|
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, resolveChartSizeOption, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W, planarProviderMaterialOfDirection } from "./scene-radiance.js";
|
|
@@ -107,7 +110,7 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
107
110
|
export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
108
111
|
"lens.diffraction", "lens.defocus", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
109
112
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
110
|
-
"output.diffractionCorrection", "output.sharpening", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
113
|
+
"output.diffractionCorrection", "output.sharpening", "output.lensCorrections", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
111
114
|
/**
|
|
112
115
|
* The options that take the exact-or-fast flag (#395), in the path style of {@link SIMULATE_CAPTURE_COST_OPTIONS}: each accepts
|
|
113
116
|
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
@@ -118,10 +121,12 @@ export const FACADE_EVIDENCE = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Ob
|
|
|
118
121
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
119
122
|
const DEFAULTS = /* @__PURE__ */ Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
120
123
|
subjectDistanceM: 5, width: 64, height: 64, format: "srgb-8", toneCurve: "identity", highlightMode: "clip",
|
|
121
|
-
whiteBalance: "scene-illuminant", jpegQuantizationStep:
|
|
124
|
+
whiteBalance: "scene-illuminant", jpegQuantizationStep: 1, pixelPitchMicrometers: 6, spectralStepNanometers: 20,
|
|
122
125
|
temporalSampleCount: 1, noiseSeed: 1, illuminantKelvin: 6500 });
|
|
123
126
|
/** Standard output sensitivity S = 10 / H (CIPA DC-004-2020 §3.2): the reference exposure in lx·s is 10 / S. */
|
|
124
127
|
const SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS = 10;
|
|
128
|
+
/** #565: the one-tap kernel that makes the image-space provider read each query at its lattice point. */
|
|
129
|
+
const UNIT_SITE_KERNEL = /* @__PURE__ */ new Float64Array([1]);
|
|
125
130
|
/** Camera equation H = 0.65·L·t/N² (CIPA DC-004-2020 §5.5 (4); 0.65 = τ·π/4 with τ = 0.828, Explanation 4). */
|
|
126
131
|
const CAMERA_EQUATION_CONSTANT = 0.65;
|
|
127
132
|
/** Neutral reference reflectance: DC-004 ties the SOS reference level to an 18 % grey (Explanation 1 and 2.2). */
|
|
@@ -179,15 +184,16 @@ function greyCardDeviationStops(fNumber, shutterSeconds, iso, illuminanceLux) {
|
|
|
179
184
|
*/
|
|
180
185
|
export const SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX = Math.PI * sosReferenceLuminance(DEFAULTS.fNumber, DEFAULTS.shutterSeconds, DEFAULTS.iso) /
|
|
181
186
|
GREY_CARD_REFLECTANCE;
|
|
182
|
-
/**
|
|
183
|
-
* Sensitive-aperture area (µm²) at which the tier's ISO signal chain applies unchanged: the default 6 µm square
|
|
184
|
-
* aperture with fill factor 1. At other areas the facade scales the electron-domain readout values by
|
|
185
|
-
* area / this reference, so the same focal-plane exposure H gives the same output code (see `isoAreaScaledReadout`).
|
|
186
|
-
*/
|
|
187
187
|
/** 2.5: the largest accepted pixel pitch, µm (44 mm ÷ 48 pixels ≈ 917 µm, a whole full-frame or larger sensor at a small output). */
|
|
188
188
|
const SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS = 1000;
|
|
189
189
|
/** Pitches above this, µm, are larger than any camera photosite: the capture is a downsized whole-frame view (assumption sensor.wholeFrameView). */
|
|
190
190
|
const SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS = 100;
|
|
191
|
+
/**
|
|
192
|
+
* Sensitive-aperture area (µm²) at which the tier's ISO signal chain applies unchanged: the default 6 µm square
|
|
193
|
+
* aperture with fill factor 1. At other areas the facade scales the electron-domain readout values by
|
|
194
|
+
* area / this reference, so the same focal-plane exposure H gives the same output code (the `chargeScale` of
|
|
195
|
+
* `characterizeReadout`, src/sensor/sensor-characteristics.ts).
|
|
196
|
+
*/
|
|
191
197
|
const ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS = DEFAULTS.pixelPitchMicrometers * DEFAULTS.pixelPitchMicrometers;
|
|
192
198
|
/** Demosaic border, native sites per side. */
|
|
193
199
|
const MARGIN = 1;
|
|
@@ -340,17 +346,6 @@ const TEXT_METADATA_LAYOUT = /* @__PURE__ */ Object.freeze({
|
|
|
340
346
|
png: "One tEXt (printable Latin-1) or iTXt (UTF-8) chunk per field, keywords Description, Software, Copyright, Comment, before the first IDAT",
|
|
341
347
|
dng: "ImageDescription (statement, then the description) and Copyright when printable ASCII; an XMP packet in tag 700 with every field"
|
|
342
348
|
});
|
|
343
|
-
/**
|
|
344
|
-
* The readout profile with every regime's charge-domain transfer scaled by `areaScale`: system conversion gain
|
|
345
|
-
* (e⁻ per code) and pre-ADC saturation (e⁻ equivalent) are multiplied by it, so codes per unit focal-plane exposure
|
|
346
|
-
* stay those of the reference aperture. Read noise stays in electrons per pixel; codes, black level, ADC and digital
|
|
347
|
-
* gain are unchanged. The result is re-checked by the readout parser.
|
|
348
|
-
*/
|
|
349
|
-
function isoAreaScaledReadout(profile, areaScale) {
|
|
350
|
-
return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => ({ ...regime,
|
|
351
|
-
systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: regime.systemConversionGainElectronsPerCode.value * areaScale },
|
|
352
|
-
preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * areaScale } })) });
|
|
353
|
-
}
|
|
354
349
|
/** Diagnostic clock for the 2.1 stage timings: `performance.now()` where the host has it. Never affects output. */
|
|
355
350
|
export function clockMs() { return globalThis.performance?.now() ?? Date.now(); }
|
|
356
351
|
function positive(value, name) {
|
|
@@ -553,20 +548,20 @@ function resolveAutomaticSlowestShutterOption(value, context) {
|
|
|
553
548
|
"reciprocal rule is a rule of thumb and is not used.");
|
|
554
549
|
return report;
|
|
555
550
|
}
|
|
556
|
-
function defaultProgramLine() {
|
|
551
|
+
function defaultProgramLine(wide, narrow, anchor) {
|
|
557
552
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
558
553
|
// it first opens the aperture toward f/4, then lengthens the shutter. For more light it first closes toward
|
|
559
|
-
// f/16, then shortens the shutter.
|
|
560
|
-
|
|
554
|
+
// f/16, then shortens the shutter. #583: a lens range without f/4–f/16 moves those ends to the range (wide/narrow, the line's
|
|
555
|
+
// f/4 and f/16 clamped to it): the same shape, the aperture held at the range's end and the shutter taking up the stops
|
|
556
|
+
// (stops = log2 of t/N² over the anchor's t/N²; #585 may move the anchor), still from 1/2000 s to 1.024 s.
|
|
557
|
+
const own = wide === 4 && narrow === 16;
|
|
558
|
+
return parseExposureProgramLineProfile({ schemaVersion: "0.1.0", profileId: "facade-generic-program-line" + (own ? "" : "-lens-range"), profileVersion: "1.0.0",
|
|
561
559
|
scientificStatus: "approximation", policyKind: "generic-program-line", interpolation: "log2-aperture-shutter", evidence: FACADE_EVIDENCE,
|
|
562
560
|
limitations: ["Generic educational program line authored for the capture facade; not any manufacturer's program."],
|
|
563
|
-
nodes: [
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
{ nodeId: "plus-2", opticalExposureStopsFromReference: 2, aperture: 4, shutterSeconds: 1 / 125 },
|
|
568
|
-
{ nodeId: "plus-9", opticalExposureStopsFromReference: 9, aperture: 4, shutterSeconds: 128 / 125 }
|
|
569
|
-
] });
|
|
561
|
+
nodes: (own ? [["minus-6", -6, 16, 1 / 2000], ["minus-2", -2, 16, 1 / 125], ["zero", 0, 8, 1 / 125], ["plus-2", 2, 4, 1 / 125], ["plus-9", 9, 4, 128 / 125]] :
|
|
562
|
+
[[narrow, 1 / 2000], [narrow, 1 / 125], [wide, 1 / 125], [wide, 128 / 125]].filter((_, i) => i !== 2 || wide < narrow)
|
|
563
|
+
.map(([aperture, shutterSeconds], i) => ["n" + i, Math.log2(shutterSeconds * anchor.aperture ** 2 / anchor.shutterSeconds / aperture ** 2), aperture, shutterSeconds]))
|
|
564
|
+
.map(([nodeId, opticalExposureStopsFromReference, aperture, shutterSeconds]) => ({ nodeId, opticalExposureStopsFromReference, aperture, shutterSeconds })) });
|
|
570
565
|
}
|
|
571
566
|
/** PNG colour signalling: the sRGB chunk for sRGB, ITU-T H.273 code points through cICP otherwise. */
|
|
572
567
|
function pngColourEncoding(encoding) {
|
|
@@ -744,17 +739,19 @@ export async function serializeSimulatedCaptureDngFromParts(options, parts) {
|
|
|
744
739
|
return await runCapture(options, "dng-from-parts", parts);
|
|
745
740
|
}
|
|
746
741
|
/** The whole frame's packed RAW from region parts (see `serializeSimulatedCaptureDngFromParts`). */
|
|
747
|
-
function joinNativeRawParts(value, plan, width, height) {
|
|
742
|
+
function joinNativeRawParts(value, plan, width, height, caller, fast) {
|
|
748
743
|
if (!Array.isArray(value) || value.length === 0)
|
|
749
|
-
throw new InvalidConfigurationError("
|
|
744
|
+
throw new InvalidConfigurationError(caller + " needs at least one part.");
|
|
750
745
|
const parts = value, count = width * height;
|
|
751
746
|
const codes = new Uint16Array(count), blackLevels = new Uint16Array(count), digitalSaturationCodes = new Uint16Array(count), saturationFlags = new Uint8Array(count);
|
|
752
747
|
const covered = new Uint8Array(count), white = new Uint16Array(4);
|
|
753
|
-
const bad = (k, what) => { throw new InvalidConfigurationError("
|
|
748
|
+
const bad = (k, what) => { throw new InvalidConfigurationError("Part " + k + ": " + what); };
|
|
754
749
|
parts.forEach((part, k) => {
|
|
755
750
|
const rect = part?.region?.nativeRect, raw = part?.raw;
|
|
756
751
|
if (!rect || !raw)
|
|
757
752
|
bad(k, "needs raw and region.nativeRect (a region render's result).");
|
|
753
|
+
if (part.fast === true && !fast)
|
|
754
|
+
bad(k, "a fast method rendered it, but this request resolves to the exact methods (fast: false, the default for a DNG, JPEG or PNG). Render the parts with fast: false, or pass fast: true here.");
|
|
758
755
|
const { x, y, width: w, height: h } = rect;
|
|
759
756
|
if (![x, y, w, h].every(Number.isSafeInteger) || x < 0 || y < 0 || w < 1 || h < 1 || x + w > width || y + h > height)
|
|
760
757
|
bad(k, "its nativeRect is not inside the frame's native raster.");
|
|
@@ -792,9 +789,9 @@ function joinNativeRawParts(value, plan, width, height) {
|
|
|
792
789
|
}
|
|
793
790
|
});
|
|
794
791
|
if (covered.includes(0))
|
|
795
|
-
throw new InvalidConfigurationError("The
|
|
792
|
+
throw new InvalidConfigurationError("The parts do not cover every native site of the frame.");
|
|
796
793
|
if (white.includes(0))
|
|
797
|
-
throw new InvalidConfigurationError("The
|
|
794
|
+
throw new InvalidConfigurationError("The parts lack a white level for a CFA phase.");
|
|
798
795
|
return { plan, codes, blackLevels, digitalSaturationCodes, saturationFlags, effectiveSaturationCodes: white };
|
|
799
796
|
}
|
|
800
797
|
/**
|
|
@@ -1027,8 +1024,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1027
1024
|
}
|
|
1028
1025
|
else if (lensOptions.focusScatter !== undefined)
|
|
1029
1026
|
fail("lens.focusScatter needs lens.catalogue: the focus scatter is a catalogue lens's own.");
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1027
|
+
const listed = prime ?? catalogue, lensRange = listed ? { id: listed.lensId, name: catalogue ? catalogue.name : listed.lensId, focalLengthMm: listed.focalLengthMm,
|
|
1028
|
+
widestFNumber: listed.widestFNumber, narrowestFNumber: listed.narrowestFNumber } : ideal;
|
|
1032
1029
|
const lensExposure = lensRange ? { ...lens.exposure, profileId: "photivra-" + lensRange.id + ":exposure",
|
|
1033
1030
|
focalLengthMmRange: { ...lens.exposure.focalLengthMmRange, value: { minimum: lensRange.focalLengthMm, maximum: lensRange.focalLengthMm } },
|
|
1034
1031
|
aperture: { ...lens.exposure.aperture, widestAvailableFNumber: { kind: "constant", fNumber: { value: lensRange.widestFNumber, evidence: FACADE_EVIDENCE } },
|
|
@@ -1148,11 +1145,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1148
1145
|
fail("lens.defocus cannot be combined with lens.pupilSampling: the pupil rays already render defocus, so it would be counted twice.");
|
|
1149
1146
|
if (lensPsf)
|
|
1150
1147
|
fail("lens.defocus cannot be combined with lens.psf: a caller PSF declares its own defocus, so it could be counted twice.");
|
|
1151
|
-
// #549: with a catalogue lens
|
|
1152
|
-
// options (#553) shape the lens.defocus kernel only, so they are refused rather than dropped (a follow-up for a later release).
|
|
1148
|
+
// #549, #567: with a catalogue lens its own bokeh kernel is convolved into its PSF (below); it sets all three bokeh options.
|
|
1153
1149
|
if (catalogue && defocusBokeh)
|
|
1154
1150
|
fail("lens.defocus.blades, sphericalAberration and catsEye cannot be combined with lens.catalogue: the catalogue lens " +
|
|
1155
|
-
"
|
|
1151
|
+
"sets its own (lens.catalogue.character). Omit the bokeh options, or use lens.focalLengthMm instead of the catalogue lens.");
|
|
1156
1152
|
if (!catalogue)
|
|
1157
1153
|
defocusModel = "geometric-disc";
|
|
1158
1154
|
}
|
|
@@ -1181,7 +1177,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1181
1177
|
const diffractionObjectSpace = diffractionRadiusOption !== undefined && !rotating && !lensFieldMapping && !vignetting;
|
|
1182
1178
|
// Output and sensor
|
|
1183
1179
|
const outputOptions = allowKeys(top.output, "output", ["width", "height", "format", "toneCurve", "toneCurveParameters", "highlightMode", "exposureCompensationStops", "whiteBalance", "jpegQuantizationStep",
|
|
1184
|
-
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening"]);
|
|
1180
|
+
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening", "lensCorrections"]);
|
|
1185
1181
|
// Opt-in large images: a caller-raised native pixel limit. Without it the 4094-pixel sides are unchanged.
|
|
1186
1182
|
const raisedPixels = outputOptions.maximumPixels === undefined ? undefined : integerIn(outputOptions.maximumPixels, "output.maximumPixels", 1, NATIVE_RAW_LIMITS.ceilingPixels);
|
|
1187
1183
|
const maximumSide = raisedPixels === undefined ? 4094 : NATIVE_RAW_LIMITS.maximumDimension;
|
|
@@ -1190,10 +1186,29 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1190
1186
|
const format = pick(outputOptions, "format", "output.format", DEFAULTS.format, "8-bit sRGB samples.");
|
|
1191
1187
|
if (!["srgb-8", "srgb-16", "jpeg", "png", "dng"].includes(format))
|
|
1192
1188
|
fail("output.format must be srgb-8, srgb-16, jpeg, png or dng.");
|
|
1193
|
-
|
|
1189
|
+
const correctionSelection = (value, name) => {
|
|
1190
|
+
if (value === undefined)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
const o = allowKeys(value, name, ["distortion", "lateralColor", "vignetting"]), selection = {};
|
|
1193
|
+
for (const key of ["distortion", "lateralColor", "vignetting"]) {
|
|
1194
|
+
if (o[key] !== undefined && typeof o[key] !== "boolean")
|
|
1195
|
+
fail(name + "." + key + " must be true or false.");
|
|
1196
|
+
selection[key] = o[key] === true;
|
|
1197
|
+
}
|
|
1198
|
+
if (!Object.values(selection).includes(true))
|
|
1199
|
+
return undefined;
|
|
1200
|
+
if (!catalogue)
|
|
1201
|
+
fail(name + " needs lens.catalogue: the corrections are the catalogue lens's own profile.");
|
|
1202
|
+
return selection;
|
|
1203
|
+
};
|
|
1204
|
+
let dngCapturedAtUtc, dngOpcodeSelection;
|
|
1194
1205
|
if (format === "dng") {
|
|
1195
|
-
const d = allowKeys(outputOptions.dng, "output.dng", ["capturedAtUtc"]);
|
|
1206
|
+
const d = allowKeys(outputOptions.dng, "output.dng", ["capturedAtUtc", "lensCorrectionOpcodes"]);
|
|
1196
1207
|
dngCapturedAtUtc = d.capturedAtUtc;
|
|
1208
|
+
// #587: the corrections as DNG opcodes, which apply to the whole frame's ActiveArea.
|
|
1209
|
+
dngOpcodeSelection = correctionSelection(d.lensCorrectionOpcodes, "output.dng.lensCorrectionOpcodes");
|
|
1210
|
+
if (dngOpcodeSelection && outputOptions.region !== undefined)
|
|
1211
|
+
fail("output.dng.lensCorrectionOpcodes needs the whole frame (no output.region).");
|
|
1197
1212
|
if (dngCapturedAtUtc === undefined)
|
|
1198
1213
|
assume("output.dng.capturedAtUtc", "default", null, "No capture time: the DNG carries no DateTimeOriginal (the engine never invents a wall clock).");
|
|
1199
1214
|
}
|
|
@@ -1249,10 +1264,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1249
1264
|
}
|
|
1250
1265
|
const exposureGain = exposureCompensationStops === 0 ? 1 : 2 ** exposureCompensationStops;
|
|
1251
1266
|
const whiteBalanceOption = pick(outputOptions, "whiteBalance", "output.whiteBalance", DEFAULTS.whiteBalance, "White balance measured from a perfect white diffuser under the scene illuminant (a custom/grey-card white balance).");
|
|
1252
|
-
const jpegStep = format === "jpeg" ? integerIn(pick(outputOptions, "jpegQuantizationStep", "output.jpegQuantizationStep", DEFAULTS.jpegQuantizationStep, "Constant JPEG quantization step
|
|
1267
|
+
const jpegStep = format === "jpeg" ? integerIn(pick(outputOptions, "jpegQuantizationStep", "output.jpegQuantizationStep", DEFAULTS.jpegQuantizationStep, "Constant JPEG quantization step 1 (not a manufacturer quality scale)."), "output.jpegQuantizationStep", 1, 255) : undefined;
|
|
1253
1268
|
if (format !== "jpeg" && outputOptions.jpegQuantizationStep !== undefined)
|
|
1254
1269
|
fail("output.jpegQuantizationStep applies only to output.format \"jpeg\".");
|
|
1255
|
-
const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing", "pixelAperture"]);
|
|
1270
|
+
const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing", "pixelAperture", ...SENSOR_CHARACTERISTIC_KEYS]);
|
|
1256
1271
|
const pitch = positive(pick(sensorOptions, "pixelPitchMicrometers", "sensor.pixelPitchMicrometers", DEFAULTS.pixelPitchMicrometers, "6 µm pixels (about a 24 MP full-frame sensor). The simulated sensor is width × pitch wide, so small images are a central crop of a large sensor, not a downscaled full frame."), "sensor.pixelPitchMicrometers");
|
|
1257
1272
|
if (pitch < 0.5 || pitch > SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS)
|
|
1258
1273
|
fail("sensor.pixelPitchMicrometers must be 0.5-" + SIMULATE_CAPTURE_PIXEL_PITCH_MAXIMUM_MICROMETERS + ".");
|
|
@@ -1261,6 +1276,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1261
1276
|
fail("sensor.spectralStepNanometers must be 1-100.");
|
|
1262
1277
|
const temporalSampleCount = integerIn(rotating ? pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", ROTATING_TEMPORAL_SAMPLE_COUNT, "Eight temporal midpoints across each site's exposure window sample the camera rotation (motion blur); raise it for long exposures or fast rotation.") :
|
|
1263
1278
|
pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", DEFAULTS.temporalSampleCount, "One temporal sample: the static scene and still camera make more samples unnecessary."), "sensor.temporalSampleCount", 1, 256);
|
|
1279
|
+
const characteristics = parseSensorCharacteristics(sensorOptions);
|
|
1264
1280
|
const noiseSeed = integerIn(pick(sensorOptions, "noiseSeed", "sensor.noiseSeed", DEFAULTS.noiseSeed, "Fixed noise seed 1 (deterministic output)."), "sensor.noiseSeed", 0, 0xffffffff);
|
|
1265
1281
|
const antiAliasing = sensorOptions.antiAliasing === undefined ? undefined : parseAntiAliasing(sensorOptions.antiAliasing, pitch);
|
|
1266
1282
|
const antiAliasingFast = antiAliasing ? fastOverride(sensorOptions.antiAliasing, "sensor.antiAliasing") : undefined;
|
|
@@ -1303,6 +1319,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1303
1319
|
unsupported("output.highlightMode \"reconstruct\"", "the view supports the per-channel highlight clip.");
|
|
1304
1320
|
if (outputOptions.diffractionCorrection !== undefined || outputOptions.sharpening !== undefined)
|
|
1305
1321
|
unsupported("output.diffractionCorrection or output.sharpening", "the development detail stages filter the whole developed capture image.");
|
|
1322
|
+
if (outputOptions.lensCorrections !== undefined)
|
|
1323
|
+
unsupported("output.lensCorrections", "the corrections resample the developed capture image.");
|
|
1306
1324
|
if (defocusBokeh?.catsEye)
|
|
1307
1325
|
unsupported("lens.defocus.catsEye", "the expected-signal tables hold one kernel per spectral node, and the cat's eye varies over the field; " +
|
|
1308
1326
|
"simulateCapture renders it. lens.defocus.blades and lens.defocus.sphericalAberration are supported.");
|
|
@@ -1326,10 +1344,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1326
1344
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
1327
1345
|
fail("output.demosaic must be bilinear or ahd.");
|
|
1328
1346
|
const ahd = demosaicOption === "ahd";
|
|
1329
|
-
// #395: exact or fast per option
|
|
1347
|
+
// #395: exact or fast per option: the option record's `fast`, else the top-level one, else (2.20.0) the format's default: fast for
|
|
1348
|
+
// sRGB samples and the display view, exact for the exports (DNG, JPEG, PNG) and the ideal reference. Fast methods report true when they run.
|
|
1330
1349
|
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1331
1350
|
fail("fast must be true or false.");
|
|
1332
|
-
const fastTop = top.fast
|
|
1351
|
+
const fastTop = resolveFast(top.fast, fastFormatDefault(format, displayView ? "display-view" : idealReference ? "ideal-reference" : "capture"));
|
|
1333
1352
|
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
1334
1353
|
["lens.catalogue", catalogue !== undefined, catalogueFast],
|
|
1335
1354
|
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast]];
|
|
@@ -1338,11 +1357,15 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1338
1357
|
if (used)
|
|
1339
1358
|
fastApplied[option] = false;
|
|
1340
1359
|
// #568: the catalogue's fast method, when requested (its record's flag, else the top-level one); whether it runs is decided with the RAW path.
|
|
1341
|
-
const catalogueFastRequested = catalogue !== undefined && (catalogueFast
|
|
1360
|
+
const catalogueFastRequested = catalogue !== undefined && resolveFast(catalogueFast, fastTop);
|
|
1342
1361
|
let fastReport = Object.freeze({
|
|
1343
1362
|
requested: fastTop || [diffractionFast, catalogueFast, antiAliasingFast, boxFast, demosaicFast].some((value) => value === true),
|
|
1344
|
-
applied: Object.freeze({ ...fastApplied }), assurance: "engine-reference"
|
|
1363
|
+
defaultedFrom: top.fast === undefined ? "format" : "request", applied: Object.freeze({ ...fastApplied }), assurance: "engine-reference"
|
|
1345
1364
|
});
|
|
1365
|
+
const markFast = (option) => {
|
|
1366
|
+
fastApplied[option] = true;
|
|
1367
|
+
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
1368
|
+
};
|
|
1346
1369
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
1347
1370
|
fail("output.borderHandling must be margin or mirror.");
|
|
1348
1371
|
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
@@ -1355,23 +1378,36 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1355
1378
|
fail("output.demosaic \"ahd\" with mirror borders needs an output of at least 3 × 3 pixels.");
|
|
1356
1379
|
// Geometry: the demosaic border (if any) on each side; output crop is the active area.
|
|
1357
1380
|
const nativeWidth = width + 2 * margin, nativeHeight = height + 2 * margin;
|
|
1381
|
+
// Lens corrections (#570): the catalogue lens's corrections on the developed linear colour; all off is no option at all.
|
|
1382
|
+
let lensCorrection;
|
|
1383
|
+
const lensCorrectionSelection = correctionSelection(outputOptions.lensCorrections, "output.lensCorrections");
|
|
1384
|
+
if (lensCorrectionSelection) {
|
|
1385
|
+
if (idealReference)
|
|
1386
|
+
fail("simulateIdealReference does not take output.lensCorrections: a reference has no camera development.");
|
|
1387
|
+
lensCorrection = lensKit.prepareDevelopedLensCorrection(catalogue, lensCorrectionSelection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
1388
|
+
}
|
|
1358
1389
|
// Region (2.3): a rectangle of the whole frame. Its native sites are the region plus the demosaic reach on each side
|
|
1359
1390
|
// (clipped to the raster, where mirror borders apply exactly as in the whole render), with the origin rounded down to
|
|
1360
1391
|
// the 2 × 2 CFA repeat so the region raster keeps the frame's CFA phase.
|
|
1361
|
-
let region, regionNative;
|
|
1392
|
+
let region, regionNative, imageRegion;
|
|
1362
1393
|
if (outputOptions.region !== undefined) {
|
|
1363
1394
|
const r = allowKeys(outputOptions.region, "output.region", ["x", "y", "width", "height"]);
|
|
1364
1395
|
const x = integerIn(r.x, "output.region.x", 0, width - 1), y = integerIn(r.y, "output.region.y", 0, height - 1);
|
|
1365
1396
|
region = { x, y, width: integerIn(r.width, "output.region.width", 1, width - x), height: integerIn(r.height, "output.region.height", 1, height - y) };
|
|
1397
|
+
// #570: a corrected region develops the frame pixels its corrected pixels read.
|
|
1398
|
+
if (lensCorrection) {
|
|
1399
|
+
imageRegion = region;
|
|
1400
|
+
region = lensKit.developedLensCorrectionSourceRect(lensCorrection, region);
|
|
1401
|
+
}
|
|
1366
1402
|
const reach = margin > 0 ? margin : ahd ? AHD_MARGIN : builtInDemosaic ? builtInDemosaic.kernelReachSites : MARGIN;
|
|
1367
1403
|
let x0 = Math.max(0, region.x + margin - reach), y0 = Math.max(0, region.y + margin - reach);
|
|
1368
1404
|
x0 -= x0 % 2;
|
|
1369
1405
|
y0 -= y0 % 2;
|
|
1370
1406
|
regionNative = { x: x0, y: y0, width: Math.min(nativeWidth, region.x + region.width + margin + reach) - x0, height: Math.min(nativeHeight, region.y + region.height + margin + reach) - y0 };
|
|
1371
1407
|
}
|
|
1372
|
-
|
|
1408
|
+
let outputWidth = region ? region.width : width, outputHeight = region ? region.height : height;
|
|
1373
1409
|
if (region && regionNative)
|
|
1374
|
-
assume("output.region", "model", { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } }, "Only this rectangle of the " + width + " × " + height + " frame is rendered. Framing, metering, exposure, diffraction kernels and every site's physics, noise seeds and exposure window (rolling-shutter row timing) are those of the whole frame (absolute native indices), " +
|
|
1410
|
+
assume("output.region", "model", { ...imageRegion ?? region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } }, "Only this rectangle of the " + width + " × " + height + " frame is rendered. Framing, metering, exposure, diffraction kernels and every site's physics, noise seeds and exposure window (rolling-shutter row timing) are those of the whole frame (absolute native indices), " +
|
|
1375
1411
|
"and the native rectangle includes the demosaic reach on each side (" + (margin > 0 ? margin : "mirror-border") + "), so the pixels are byte-identical to the same pixels of the whole render. " +
|
|
1376
1412
|
// 2.6: with lens.diffraction the convolution is limited to the region; the sentence of 2.3–2.5 is kept verbatim otherwise, so
|
|
1377
1413
|
// region results without diffraction stay byte-identical to 2.5.0.
|
|
@@ -1555,13 +1591,16 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1555
1591
|
assume("exposure.compensationStops", "default", 0, "No exposure compensation.");
|
|
1556
1592
|
// #569: a lens whose range excludes f/8 (the catalogue's PV 800 9, f/9–f/32; an ideal lens with a narrowed range) takes the same reference
|
|
1557
1593
|
// exposure at the aperture of its range nearest f/8, N0 with t = 1/125 s × (N0/8)², which has the same N²/t and so the same reference
|
|
1558
|
-
// luminance; every other request keeps f/8, 1/125 s.
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1594
|
+
// luminance; every other request keeps f/8, 1/125 s. #585: when that t is shorter than the body's shortest shutter (N0 below about f/1),
|
|
1595
|
+
// the anchor takes the shortest shutter instead, a lower N²/t; the meter, the resolvers and the program line all measure from the anchor.
|
|
1596
|
+
// An f-number clamped to the lens's range (#583 limits the program line's f/4 and f/16 with it).
|
|
1597
|
+
const clamp = (n) => lensRange ? Math.min(lensRange.narrowestFNumber, Math.max(lensRange.widestFNumber, n)) : n, referenceAperture = clamp(DEFAULTS.fNumber);
|
|
1598
|
+
// (N0/8)² is exactly 1 at f/8, so f/8 keeps 1/125 s exactly.
|
|
1599
|
+
const sameShutter = DEFAULTS.shutterSeconds * (referenceAperture / DEFAULTS.fNumber) ** 2;
|
|
1600
|
+
const reference = { aperture: referenceAperture, shutterSeconds: Math.max(sameShutter, capabilities.shutter.minimumSeconds), iso: DEFAULTS.iso };
|
|
1562
1601
|
const referenceLuminance = sosReferenceLuminance(reference.aperture, reference.shutterSeconds, reference.iso);
|
|
1563
1602
|
assume("exposure.referenceExposure", "model", { ...reference, meteredLuminanceCdPerSquareMeter: referenceLuminance }, "Exposure anchor: f/8, 1/125 s, ISO 100 gives the CIPA DC-004 standard output sensitivity reference exposure H = 10/S = 0.1 lx·s for an average scene luminance of 10·N²/(0.65·t·S) ≈ 1230.77 cd/m² (camera equation H = 0.65·L·t/N²)" +
|
|
1564
|
-
(referenceAperture === DEFAULTS.fNumber ? "." : "; the lens excludes f/8: the same N²/t at f/" + referenceAperture + "."));
|
|
1603
|
+
(referenceAperture === DEFAULTS.fNumber ? "." : "; the lens excludes f/8: " + (reference.shutterSeconds > sameShutter ? "the shortest shutter" : "the same N²/t") + " at f/" + referenceAperture + "."));
|
|
1565
1604
|
const meterProfile = body.metering[0].profile, zones = 16, zoneSamples = [];
|
|
1566
1605
|
let luminanceSum = 0;
|
|
1567
1606
|
for (let j = 0; j < zones; j++)
|
|
@@ -1593,7 +1632,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1593
1632
|
let resolution;
|
|
1594
1633
|
let fNumber, shutterSeconds;
|
|
1595
1634
|
const checkFNumber = () => {
|
|
1596
|
-
if (
|
|
1635
|
+
if (clamp(fNumber) !== fNumber)
|
|
1597
1636
|
fail("lens.fNumber " + fNumber + " is outside the " + lensRange.name + " range f/" + lensRange.widestFNumber + "–f/" + lensRange.narrowestFNumber + ".");
|
|
1598
1637
|
};
|
|
1599
1638
|
if (mode === "manual" || mode === "aperture-priority") {
|
|
@@ -1695,8 +1734,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1695
1734
|
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1696
1735
|
}
|
|
1697
1736
|
else {
|
|
1698
|
-
|
|
1699
|
-
|
|
1737
|
+
// #583: a lens range without f/4–f/16 limits the line to it; every other request keeps the line and its assumption.
|
|
1738
|
+
const programLine = defaultProgramLine(clamp(4), clamp(16), reference);
|
|
1739
|
+
// A limited line (#583) records its nodes under the profile id "facade-generic-program-line-lens-range".
|
|
1740
|
+
assume("exposure.programLine", "model", programLine.nodes.length > 4 ? programLine.profileId : programLine, "Engine-authored generic program line (f/16 1/2000 s … f/8 1/125 s … f/4 1.024 s); not a manufacturer program.");
|
|
1741
|
+
resolution = resolveProgramAutoExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, programLine,
|
|
1700
1742
|
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1701
1743
|
{ kind: "manual", iso } });
|
|
1702
1744
|
}
|
|
@@ -1715,7 +1757,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1715
1757
|
// its distortion and vignetting profiles over the field the PSF and the queries reach, and its PSF profile.
|
|
1716
1758
|
let catalogueTaps, catalogueDefocusApplied = false, catalogueKernelRadius = 0;
|
|
1717
1759
|
if (catalogue && lensKit) {
|
|
1718
|
-
const { GENERIC_LENS_EVIDENCE, genericLensDistortionProfile, genericLensVignettingProfile, resolveGenericLens, simulateGenericLensFocusError, createGenericLensPsfProfile, genericLensPsfKernelRadius, GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES } = lensKit;
|
|
1760
|
+
const { GENERIC_LENS_EVIDENCE, genericLensDistortionProfile, genericLensVignettingProfile, resolveGenericLens, simulateGenericLensFocusError, createGenericLensPsfProfile, genericLensPsfKernelRadius, GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES, genericLensBokehKernel } = lensKit;
|
|
1719
1761
|
checkFNumber();
|
|
1720
1762
|
if (lensOptions.focusScatter !== undefined) {
|
|
1721
1763
|
const scatterOptions = allowKeys(lensOptions.focusScatter, "lens.focusScatter", ["seed", "shotIndex"]);
|
|
@@ -1737,16 +1779,25 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1737
1779
|
catch {
|
|
1738
1780
|
fail("lens.defocus: " + defocusDiscLimitMessage(disc.diameterSites, disc.diameterMicrometers));
|
|
1739
1781
|
}
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1782
|
+
// #567: the lens's bokeh kernel (blades, its W040, a cat's eye) instead of the plain disc.
|
|
1783
|
+
// The cat's eye (its finer fast grid) is lens.catalogue's exact method; fast leaves it out.
|
|
1784
|
+
const catsEye = !catalogueFastRequested;
|
|
1785
|
+
if (run !== "ideal-reference" && disc.diameterSites > 0) {
|
|
1786
|
+
extraKernel = genericLensBokehKernel(character, disc.diameterSites, subjectDistanceM > focusDistanceM ? 1 : -1, pitch, catsEye);
|
|
1743
1787
|
catalogueDefocusApplied = true;
|
|
1788
|
+
if (!catsEye && character.character.bokeh.catsEye) {
|
|
1789
|
+
markFast("lens.catalogue");
|
|
1790
|
+
assume("fast.lens.catalogue.catsEye", "model", { method: "bokeh-without-cats-eye" }, "Fast: the lens's bokeh leaves out its cat's eye (the pupil clipped by field position), " +
|
|
1791
|
+
"so the kernel is the axis kernel everywhere; the exact method (fast: false, the default for DNG, JPEG and PNG) shapes it at each field point.");
|
|
1792
|
+
}
|
|
1744
1793
|
}
|
|
1794
|
+
const k = extraKernel?.axis;
|
|
1745
1795
|
assume("lens.defocus", "model", { model: "geometric-disc", scientificStatus: "approximation", diameterMicrometers: disc.diameterMicrometers, diameterSites: disc.diameterSites,
|
|
1746
|
-
kernelRadiusSites: disc.diameterSites === 0 ? null : discRadius, fNumber, focusDistanceM, subjectDistanceM, samplePitchMicrometers: pitch,
|
|
1747
|
-
defocusCircleModel: { id: "thin-lens-defocus-circle", version: "1.1.0" }, execution: "catalogue-lens-psf"
|
|
1796
|
+
kernelRadiusSites: disc.diameterSites === 0 ? null : k ? k.radius : discRadius, fNumber, focusDistanceM, subjectDistanceM, samplePitchMicrometers: pitch,
|
|
1797
|
+
defocusCircleModel: { id: "thin-lens-defocus-circle", version: "1.1.0" }, execution: "catalogue-lens-psf",
|
|
1798
|
+
...(k ? { bokeh: { catsEye: catsEye && !!character.character.bokeh.catsEye, method: k.method, sphericalAberrationSites: k.sphericalAberrationSites, axisOpenAreaRatio: k.openAreaRatio } } : {}) }, "Geometric defocus of the flat scene plane (an approximation): the thin-lens blur circle c = f²·|s − d| / (N·d·(s − f)) (calculateDefocusCircle 1.1.0) as a uniform disc sampled on the site lattice. " +
|
|
1748
1799
|
(disc.diameterSites === 0 ? "In focus: no disc is applied." : run === "ideal-reference" ? "Resolved but not applied: the ideal reference has no camera effects." :
|
|
1749
|
-
"With the catalogue lens the disc is convolved into every node of the lens's PSF (a further approximation: the defocused aberrated wave PSF is not exactly the in-focus PSF convolved with the disc) and applied through the environment PSF quadrature."));
|
|
1800
|
+
"With the catalogue lens the disc is shaped by the lens's bokeh (#567, #553's kernel; see lens.catalogue.character) and convolved into every node of the lens's PSF (a further approximation: the defocused aberrated wave PSF is not exactly the in-focus PSF convolved with the disc) and applied through the environment PSF quadrature."));
|
|
1750
1801
|
}
|
|
1751
1802
|
// An upper bound of the PSF's radius (the built window may be narrower): the field reach below uses it.
|
|
1752
1803
|
const kernelRadius = genericLensPsfKernelRadius({ lens: character, focusDistanceM, sitePitchMicrometers: pitch }) + (extraKernel ? extraKernel.radius : 0);
|
|
@@ -1790,23 +1841,32 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1790
1841
|
}
|
|
1791
1842
|
assume("lens.catalogue.character", "model", { lensId: catalogue.lensId, fNumber, focusDistanceM, magnification: c.magnification, closeFocusGain: c.closeFocusGain, pupilScale: c.pupilScale,
|
|
1792
1843
|
aberrationsWaves550: { ...c.aberrations }, lateralColor: { ...c.lateralColor }, distortionCorner: c.distortion.corner, vignettingCornerStops: c.vignetting.cornerStops,
|
|
1793
|
-
notRendered: { strayLight: { ...c.strayLight }, breathing: { ...c.breathing }, focusScatterDepthOfFocusFraction: c.focusScatterDepthOfFocusFraction
|
|
1844
|
+
bokeh: c.bokeh, notRendered: { strayLight: { ...c.strayLight }, breathing: { ...c.breathing }, focusScatterDepthOfFocusFraction: c.focusScatterDepthOfFocusFraction },
|
|
1794
1845
|
psf: psfReport, profiles: { vignetting, distortion: distortion.profile, maximumNormalizedRadius: reachRadius } }, "The catalogue lens's character, a labelled approximation of a generic lens (docs/LENS_CATALOGUE.md): Seidel wave aberrations at the frame corner (spherical, balanced by defocus; coma; astigmatism; field curvature), " +
|
|
1795
1846
|
"scaled from the widest aperture by the pupil ratio a = N0/N (a⁴, a³, a², a²) and by the close-focus factor; axial colour as a defocus and lateral colour as a radial shift, both with the normalised Cauchy dispersion. " +
|
|
1796
1847
|
(run === "ideal-reference" ? "Not applied: the ideal reference has no camera effects. " :
|
|
1797
1848
|
"Rendered as a sampled PSF (scalar Fraunhofer diffraction of the aberrated circular pupil, integrated over each site cell, on a 5 × 5 field grid × 5 wavelengths with centroid-aligned interpolation) " +
|
|
1798
1849
|
"through the environment PSF quadrature (one scene query per kernel tap), with the lens's radial distortion (r_d = r·(1 + k1ρ²)) and relative illumination (E = (1 − bρ²)²) as lens field mapping and field throughput" +
|
|
1799
1850
|
(catalogue.kind === "fisheye" ? ", and the equisolid projection; directions at or beyond 90° see no scene (zero radiance), so a circular fisheye's image ends at its 180° circle" : "") + ". ") +
|
|
1800
|
-
"Flare
|
|
1851
|
+
"Flare and focus breathing are reported, not rendered; the bokeh shapes lens.defocus; ρ is normalised to the frame's half-diagonal, so the lens is scaled to the simulated frame like lens.prime.");
|
|
1801
1852
|
}
|
|
1802
1853
|
// #492: the capture applies the disc as an object-space convolution, which needs every disc point a site sees to be another site's scene point.
|
|
1803
1854
|
// #548: a lens field mapping, vignetting or camera rotation breaks that, so the capture then applies the disc in image space, on the sensor's
|
|
1804
1855
|
// site lattice (defocus-image-space-provider.ts). #563: the display view reads the same geometry in any focus (display-view-lens-signal.ts).
|
|
1805
1856
|
const imageSpaceOptions = [...(lensFieldMapping ? [lensOptions.distortion !== undefined ? (lensOptions.projection !== undefined ? "lens.projection, lens.distortion" : "lens.distortion") : "lens.projection"] : []),
|
|
1806
1857
|
...(vignetting ? ["lens.vignetting"] : []), ...(rotating ? ["camera rotation"] : [])];
|
|
1807
|
-
const lensGeometry = (defocusModel !== undefined || displayView) && imageSpaceOptions.length > 0 ? prepareDefocusImageSpaceGeometry({ focalLengthMm, focusDistanceM,
|
|
1858
|
+
const lensGeometry = (defocusModel !== undefined || displayView || diffractionRadiusOption !== undefined) && imageSpaceOptions.length > 0 ? prepareDefocusImageSpaceGeometry({ focalLengthMm, focusDistanceM,
|
|
1808
1859
|
angularVelocityRadPerSec: angular, ...(lensFieldMapping ? { lensMapping: prepareEnvironmentLensFieldMapping(lensFieldMapping) } : {}), ...(vignetting ? { vignetting } : {}) }) : undefined;
|
|
1809
1860
|
const defocusGeometry = defocusModel !== undefined ? lensGeometry : undefined;
|
|
1861
|
+
// #565: the in-focus PSF quadrature reads each tap's source on this lattice; with rotation under a rolling shutter (each row has its own
|
|
1862
|
+
// instants, so a band per instant, about 1.2× the time) that is lens.diffraction's exact method, and each tap's own point its fast one.
|
|
1863
|
+
const rollingFastTaps = !displayView && rotating && shutterKind === "rolling" && diffractionRadiusOption !== undefined && lensGeometry !== undefined && resolveFast(diffractionFast, fastTop);
|
|
1864
|
+
if (rollingFastTaps) {
|
|
1865
|
+
markFast("lens.diffraction");
|
|
1866
|
+
assume("fast.lens.diffraction", "model", { method: "per-tap-source-points" }, "Fast: under rolling-shutter rotation each PSF tap reads the scene at its own source point, " +
|
|
1867
|
+
"not at its site-lattice point, so a tap exactly on a chart boundary can resolve to the other material (#565); elsewhere the values agree to rounding. The exact method is fast: false.");
|
|
1868
|
+
}
|
|
1869
|
+
const latticeTaps = lensGeometry && !rollingFastTaps;
|
|
1810
1870
|
// #553: the shaped kernel (on axis) and, with a cat's eye, its field zones; built when the disc is resolved out of focus.
|
|
1811
1871
|
let bokehAxisKernel, bokehZones;
|
|
1812
1872
|
const bokehSummary = defocusBokeh === undefined ? undefined : (disc) => {
|
|
@@ -1881,18 +1941,23 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1881
1941
|
// (EMVA 1288 Release 4.0, Eq. 3). The tier's signal chain is anchored to the 6 µm aperture; at another area the
|
|
1882
1942
|
// charge-domain readout scales with the area so the same H gives the same code. At 6 µm nothing is changed.
|
|
1883
1943
|
const areaScale = pitch * pitch / ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS;
|
|
1944
|
+
// #555: a full well replaces the area-scaled base-ISO saturation; the charge-domain transfer scales by the same rule.
|
|
1945
|
+
const fullWell = characteristics?.fullWell, fullWellElectrons = fullWell && (fullWell.link === "none" ? fullWell.electrons : fullWell.electrons * (pitch / (fullWell.referencePixelPitchMicrometers ?? 6)) ** 2);
|
|
1946
|
+
if (fullWellElectrons !== undefined && !(fullWellElectrons >= 100 && fullWellElectrons <= 1e10))
|
|
1947
|
+
fail("sensor.fullWell must give 100 to 1e10 electrons at the pixel pitch (" + fullWellElectrons + ").");
|
|
1948
|
+
const chargeScale = fullWellElectrons === undefined ? areaScale : fullWellElectrons / body.readout[0].profile.regimes[0].preAdcSaturationElectronEquivalent.value;
|
|
1884
1949
|
const regimeByChannel = {};
|
|
1885
1950
|
for (const channel of CHANNELS) {
|
|
1886
1951
|
const asset = body.readout.find((a) => a.profile.channelId === channel) ?? fail("Camera readout lacks channel " + channel + ".");
|
|
1887
1952
|
const chain = resolveGenericIsoSignalChain({ profile: body.signalChain, isoCapability: body.iso, requestedIsoSetting: { kind: "standard", exposureIndex: resolvedIso },
|
|
1888
1953
|
captureModeId: IDS.mode, readoutProfile: asset.profile });
|
|
1889
|
-
regimeByChannel[channel] = { readoutProfile:
|
|
1954
|
+
regimeByChannel[channel] = { readoutProfile: characterizeReadout(asset.profile, chargeScale, characteristics), regimeId: chain.readout.regime.regimeId };
|
|
1890
1955
|
}
|
|
1891
|
-
const capacityRegimes = (
|
|
1956
|
+
const capacityRegimes = characterizeReadout(body.readout[0].profile, chargeScale, characteristics).regimes;
|
|
1892
1957
|
const baseRegime = capacityRegimes[0];
|
|
1893
1958
|
const capacityElectrons = Math.max(...capacityRegimes.map((r) => r.preAdcSaturationElectronEquivalent.value), baseRegime.preAdcSaturationElectronEquivalent.value);
|
|
1894
1959
|
assume("sensor.capacityElectrons", "derived", capacityElectrons, "Physical charge capacity set to the body's largest declared pre-ADC saturation (its base-ISO saturation signal), so storage never clips before the declared readout white.");
|
|
1895
|
-
if (areaScale !== 1) {
|
|
1960
|
+
if (areaScale !== 1 && !fullWell) {
|
|
1896
1961
|
assume("sensor.isoAreaNormalization", "model", { referenceApertureAreaSquareMicrometers: ISO_REFERENCE_APERTURE_AREA_SQUARE_MICROMETERS,
|
|
1897
1962
|
apertureAreaSquareMicrometers: pitch * pitch, areaScale }, "Approximation. The tier's ISO signal chain is declared for a 6 µm square aperture. Standard output sensitivity S = 10/H (CIPA DC-004-2020 §3.2) is defined at the output, " +
|
|
1898
1963
|
"independent of pixel size, and the collected charge is proportional to the aperture area (EMVA 1288 Release 4.0, Eq. 3), so the system conversion gain (e⁻ per code) and the " +
|
|
@@ -1901,6 +1966,15 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1901
1966
|
"the aperture area: a design consequence of holding the tier's ISO rating and highlight headroom fixed, not a measured full-well model (published sensor data grow less than " +
|
|
1902
1967
|
"proportionally with area across technologies).");
|
|
1903
1968
|
}
|
|
1969
|
+
if (characteristics) {
|
|
1970
|
+
const density = capacityElectrons / (pitch * pitch);
|
|
1971
|
+
assume("sensor.characteristics", "model", { ...Object.fromEntries(SENSOR_CHARACTERISTIC_KEYS.filter((k) => sensorOptions[k] !== undefined).map((k) => [k, sensorOptions[k]])),
|
|
1972
|
+
chargeScale, sensitivityOffsetStops: Math.log2(characteristics.qeScale * areaScale / chargeScale) }, "Approximation (#555), not a measured sensor: these replace the body's values. The full well is the base-ISO charge at the RAW white level (EMVA 1288 Release 4.0 saturation capacity); " +
|
|
1973
|
+
"every ISO regime's conversion gain and pre-ADC saturation are multiplied by chargeScale. Each ISO keeps its regime, so the output per unit exposure changes by 2^sensitivityOffsetStops: " +
|
|
1974
|
+
"the rating S = 10/H (CIPA DC-004-2020 §3.2) holds only if peak QE × pixel area / full well is the body's. Read noise replaces each regime's; the high gain's smaller storage is not modelled. " +
|
|
1975
|
+
"A bit depth scales the black and saturation codes by 2^Δbits." + (density < 200 || density > 1e4 ? " Unusual for the pitch: " + density +
|
|
1976
|
+
" e⁻/µm², outside 200–10 000 (a design range around the tiers' 556–1 667, not a published bound)." : ""));
|
|
1977
|
+
}
|
|
1904
1978
|
// Whole-frame views (2.5): a pitch above 100 µm is larger than any camera photosite, so the capture is a downsized view of a frame width × pitch wide.
|
|
1905
1979
|
if (pitch > SIMULATE_CAPTURE_PHOTOSITE_PITCH_MAXIMUM_MICROMETERS) {
|
|
1906
1980
|
assume("sensor.wholeFrameView", "model", { pixelPitchMicrometers: pitch, frameWidthMm: width * pitch / 1000, frameHeightMm: height * pitch / 1000 }, "A pixel pitch above 100 µm is larger than any camera photosite, so each output pixel stands for a " + pitch + " µm square of a " + width * pitch / 1000 + " × " + height * pitch / 1000 +
|
|
@@ -1934,7 +2008,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1934
2008
|
// Shutter (2.0): the electronic rolling-shutter schedule derived from the body's rolling readout declaration.
|
|
1935
2009
|
let rollingSchedule;
|
|
1936
2010
|
if (shutterKind === "rolling") {
|
|
1937
|
-
const
|
|
2011
|
+
const bodyReadout = body.rollingReadoutTiming?.profile;
|
|
2012
|
+
// #555: sensor.readoutSeconds replaces the body's readout time and skew.
|
|
2013
|
+
const seconds = characteristics?.readoutSeconds, declared = bodyReadout && seconds ? { ...bodyReadout, captureReadoutDurationSeconds: { ...bodyReadout.captureReadoutDurationSeconds, value: seconds },
|
|
2014
|
+
spatialSamplingSkewSeconds: { ...bodyReadout.spatialSamplingSkewSeconds, value: seconds } } : bodyReadout;
|
|
1938
2015
|
if (!declared || declared.readoutMode !== "rolling")
|
|
1939
2016
|
fail("motion.shutter \"rolling\" needs a camera body with a rolling readout timing declaration.");
|
|
1940
2017
|
const readout = rollingSkewOption === undefined ? declared : { ...declared, spatialSamplingSkewSeconds: { value: rollingSkewOption, unit: "s", evidence: FACADE_EVIDENCE } };
|
|
@@ -2043,7 +2120,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2043
2120
|
const eqeSamples = (channel) => {
|
|
2044
2121
|
const out = [];
|
|
2045
2122
|
for (let nm = range.minimum; nm <= range.maximum + 1e-9; nm += eqeSpacing)
|
|
2046
|
-
out.push({ nm, value: EQE_SCALE[channel] * estimateCie1931ColorMatchingFunctions(nm)[CMF_KEY[channel]] * 555 / nm });
|
|
2123
|
+
out.push({ nm, value: EQE_SCALE[channel] * estimateCie1931ColorMatchingFunctions(nm)[CMF_KEY[channel]] * 555 / nm * (characteristics?.qeScale ?? 1) });
|
|
2047
2124
|
return out;
|
|
2048
2125
|
};
|
|
2049
2126
|
const operatingConditions = { temperatureC: 25, incidenceAngleDegreesFromNormal: 0, polarization: "unpolarized" };
|
|
@@ -2107,9 +2184,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2107
2184
|
samples: { value: [{ wavelengthNanometers: range.minimum, linearTransmissionFactor: lens.matchedReference.transmission },
|
|
2108
2185
|
{ wavelengthNanometers: range.maximum, linearTransmissionFactor: lens.matchedReference.transmission }], evidence: preset.evidence },
|
|
2109
2186
|
uncertainty: { kind: "not-quantified", limitation: "Synthetic spectrally flat transmission from the tier preset." } },
|
|
2110
|
-
|
|
2111
|
-
sensorOpticalStackIncluded: false, strayLightIncluded: false, polarizationModeled: false, wavelengthChangingBehaviorModeled: false, volumetricScatteringModeled: false,
|
|
2112
|
-
evidence: preset.evidence, limitations: [...lens.limitations, "Flat transmission applied across the lens capability by the capture facade."] };
|
|
2187
|
+
...SCENE_TO_SENSOR_BRIDGE_SCOPE, evidence: preset.evidence, limitations: [...lens.limitations, "Flat transmission applied across the lens capability by the capture facade."] };
|
|
2113
2188
|
const optics = { profile: opticsProfile, focalLengthMm, nominalFNumber: fNumber,
|
|
2114
2189
|
focus: { kind: "ideal-symmetric-thin-lens", objectDistanceM: focusDistanceM, pupilMagnificationAssumption: "unity" } };
|
|
2115
2190
|
const motion = { angularVelocityRadPerSec: angular, timeReference: "first-opening-boundary-phase", environmentDirectionConvention: "outgoing-radiance-toward-camera" };
|
|
@@ -2154,7 +2229,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2154
2229
|
(diffractionObjectSpace ? "Applied as an object-space convolution: the scene radiance on the site lattice is convolved once per spectral node (calculateIrradianceConvolution) and the provider answers each site query with it, so the site inputs declare no PSF; this equals the per-tap environment PSF quadrature for this still, distortion-free planar scene (request.diffraction.psf). " :
|
|
2155
2230
|
defocusImageSpace ? "Applied with lens.defocus as one image-space kernel per spectral node (the diffraction kernel convolved with the disc) on the sensor's site lattice (#548; see the lens.defocus assumption), " +
|
|
2156
2231
|
"the same sum as the environment PSF quadrature with that kernel. " :
|
|
2157
|
-
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice. ") +
|
|
2232
|
+
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice" + (latticeTaps ? "; each tap reads the scene at its lattice point (#565). " : ". ")) +
|
|
2158
2233
|
"No defocus, aberration or pupil clipping; " + (boxAperture ? "the pixel aperture is integrated as declared by sensor.pixelAperture." : "the sensor pixel aperture is still point-sampled at the site centre."));
|
|
2159
2234
|
}
|
|
2160
2235
|
// #492: each spectral node's kernel for lens.defocus out of focus: the disc, or with lens.diffraction the node's diffraction kernel convolved
|
|
@@ -2227,6 +2302,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2227
2302
|
limitation: "Ideal uniform circular pupil; finite ray sampling, no convergence claim." } : undefined;
|
|
2228
2303
|
const chargeSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.chargeSampling, completenessProfileId: IDS.completeness, scientificStatus: "approximation",
|
|
2229
2304
|
photoShotNoiseModel: "poisson", darkShotNoiseModel: "poisson", additionalComponentPolicies: [], evidence, limitations: ["Poisson photo and dark shot noise."] };
|
|
2305
|
+
// #555: the body's fixed-pattern noise profile, opt-in; the display view's expected-value estimate has no fixed pattern.
|
|
2306
|
+
const fixedPattern = characteristics?.fixedPatternNoise ? body.fixedPatternNoise.profile : undefined;
|
|
2307
|
+
if (fixedPattern && displayView)
|
|
2308
|
+
fail("simulateDisplayView does not render sensor.fixedPatternNoise; use simulateCapture.");
|
|
2230
2309
|
const channelAt = (x, y) => ["red", "green", "green", "blue"][(y % 2) * 2 + (x % 2)];
|
|
2231
2310
|
function siteInput(index) {
|
|
2232
2311
|
if (!Number.isSafeInteger(index) || index < 0 || index >= nativeWidth * nativeHeight)
|
|
@@ -2254,7 +2333,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2254
2333
|
capacityMeaning: "physical-charge-storage-capacity-electrons", capacityElectrons, scientificStatus: "approximation",
|
|
2255
2334
|
uncertainty: { kind: "not-quantified", limitation: "Derived from the tier's declared saturation signal." }, evidence, siteApplicability: { kind: "exact-site", site },
|
|
2256
2335
|
operatingState: { stateId: IDS.operatingState, evidence }, temperatureApplicability: { kind: "exact-reference-temperature", temperatureC: OPERATING_TEMPERATURE_C } },
|
|
2257
|
-
operatingStateId: IDS.operatingState, readoutProfile: r.readoutProfile, regimeId: r.regimeId } };
|
|
2336
|
+
operatingStateId: IDS.operatingState, readoutProfile: r.readoutProfile, regimeId: r.regimeId, ...(fixedPattern ? { fixedPatternNoise: fixedPattern } : {}) } };
|
|
2258
2337
|
}
|
|
2259
2338
|
const readTile = async (request) => ({ ...request, sites: Array.from({ length: request.width }, (_, j) => siteInput(request.y * nativeWidth + request.x + j)) });
|
|
2260
2339
|
const yieldControl = createHostYield().yieldControl;
|
|
@@ -2271,14 +2350,19 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2271
2350
|
wavelengthsNm: defocusSiteKernels.wavelengthsNm, kernelRadius: defocusSiteKernels.radius, nativeWidth, nativeHeight, outputWidth: width,
|
|
2272
2351
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { defocusMs += ms; },
|
|
2273
2352
|
...(window ? { window } : {}), ...(bokehZones ? { fieldZones: bokehZones.zones } : {}),
|
|
2274
|
-
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites
|
|
2353
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites } : {}) }) :
|
|
2275
2354
|
diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
2276
2355
|
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
2277
2356
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
2278
2357
|
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
2279
2358
|
...(window ? { window } : {}),
|
|
2280
2359
|
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
2281
|
-
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites
|
|
2360
|
+
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites } : {}) }) :
|
|
2361
|
+
// #565: the PSF quadrature reads each tap's source at its lattice point (a unit kernel on #548's lattice), so ties resolve as on the lattice.
|
|
2362
|
+
diffraction && latticeTaps ? createDefocusImageSpaceProvider({ base: provider, geometry: lensGeometry, siteKernels: () => UNIT_SITE_KERNEL,
|
|
2363
|
+
kernelRadius: 0, wavelengthsNm: diffraction.profile.axes.wavelengthNm, nativeWidth, nativeHeight, sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2364
|
+
queryOffsetsSites: [-1, 0, 1].flatMap((k) => queryOffsetsSites.map((o) => ({ x: o.x + k * diffraction.kernelRadiusSamples, y: o.y + k * diffraction.kernelRadiusSamples }))),
|
|
2365
|
+
timesPerBand: temporalSampleCount, ...(window ? { window } : {}) }) : provider;
|
|
2282
2366
|
// Built on first use: a capture that takes the tabulated path (#394 step 2) never queries it, and its kernels cost a setup.
|
|
2283
2367
|
let rawProviderBuilt;
|
|
2284
2368
|
const rawProvider = () => (rawProviderBuilt ??= diffractedProvider(regionNative));
|
|
@@ -2356,16 +2440,30 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2356
2440
|
// site centre plus the kernel radius and the largest query offset.
|
|
2357
2441
|
// #569: a catalogue lens's field-dependent PSF, read per field zone by the lens signal (display-view-lens-signal.ts, loaded on demand).
|
|
2358
2442
|
const viewLensKit = displayView && lensGeometry ? await import("./display-view-lens-signal.js") : undefined;
|
|
2443
|
+
// 2.20.0: the field zones are lens.catalogue's fast method in the view; exact resolves the PSF at every site (zones of one site).
|
|
2359
2444
|
const displayViewLensPsf = viewLensKit && catalogue && lensPsf ?
|
|
2360
|
-
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch) : undefined;
|
|
2445
|
+
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch, catalogueFastRequested ? undefined : Math.max(nativeWidth, nativeHeight)) : undefined;
|
|
2446
|
+
if (displayViewLensPsf && displayViewLensPsf.zoneSites > 1)
|
|
2447
|
+
markFast("lens.catalogue");
|
|
2448
|
+
const viewAhdFast = ahd && resolveFast(demosaicFast, fastTop);
|
|
2449
|
+
if (displayView && viewAhdFast)
|
|
2450
|
+
markFast("output.demosaic.ahd");
|
|
2361
2451
|
if (displayViewLensPsf)
|
|
2362
2452
|
assume("view.lensPsf", "model", { method: "piecewise-constant-field-zones", zoneSites: displayViewLensPsf.zoneSites, kernelRadiusSites: catalogueKernelRadius, scientificStatus: "approximation" }, "Each site takes the lens PSF at its field zone's centre, where the capture resolves it at each query point (docs/DISPLAY_VIEW.md, \"Catalogue lenses\").");
|
|
2363
2453
|
const lensSignal = viewLensKit ? viewLensKit.prepareDisplayViewLensSignal({ geometry: lensGeometry,
|
|
2364
2454
|
times: rotating ? Array.from({ length: temporalSampleCount }, (_, i) => (i + 0.5) * (shutterSeconds / temporalSampleCount)) : [0], distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2365
2455
|
pitch, imagingArea, boxSamples, spots: antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }], psf: displayViewLensPsf, zeroBeyondHorizon: catalogue?.kind === "fisheye" }, nativeWidth, nativeHeight, (diffraction ? diffraction.kernelRadiusSamples : 0) + (displayViewDefocus ? (bokehAxisKernel?.radius ?? defocusDiscKernelRadius(displayViewDefocus.diameterSites)) : 0) + catalogueKernelRadius +
|
|
2366
2456
|
Math.max(0, ...queryOffsetsSites.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))) - 0.5, catalogue ? "lens.catalogue" : imageSpaceOptions.join(", ")) : undefined;
|
|
2457
|
+
// #535: the green channel's expected charge from an 18 % Lambertian grey patch (radiance 0.18·E·S(λ)/π) next to the axis, and the sensor report;
|
|
2458
|
+
// the mid-grey figure only when the request resolves to exact (fast: false, the exports' default): it costs about 0.3 ms.
|
|
2459
|
+
let greyElectrons = fastTop ? undefined : 0;
|
|
2460
|
+
if (!idealReference && greyElectrons !== undefined)
|
|
2461
|
+
for (const [nm, a] of axisChannelResponse(displayView ? "simulateDisplayView" : "simulateCapture").electronsPerRadiance.green)
|
|
2462
|
+
greyElectrons += a * GREY_CARD_REFLECTANCE * irradianceScale * illuminant.shape.evaluate(nm) / Math.PI;
|
|
2463
|
+
const sensor = sensorReport(regimeByChannel.green.readoutProfile, regimeByChannel.green.regimeId, capacityElectrons, greyElectrons, DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, { pixelPitchMicrometers: pitch, peakQuantumEfficiency: 0.5 * (characteristics?.qeScale ?? 1),
|
|
2464
|
+
readoutSeconds: characteristics?.readoutSeconds ?? body.rollingReadoutTiming?.profile.spatialSamplingSkewSeconds?.value ?? null }, fixedPattern);
|
|
2367
2465
|
if (displayView)
|
|
2368
|
-
return await renderView({ lens: lensSignal, lensPsf: displayViewLensPsf, ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, exposureGain, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, metering, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
2466
|
+
return await renderView({ sensor, viewFast: { ahd: viewAhdFast, uniformSampling: fastTop }, lens: lensSignal, lensPsf: displayViewLensPsf, ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, exposureGain, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, metering, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
2369
2467
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
2370
2468
|
if (idealReference) {
|
|
2371
2469
|
const referenceStarted = clockMs();
|
|
@@ -2483,9 +2581,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2483
2581
|
// pupil rays or caller PSF) takes its expected electrons from the tabulated signal, verified per CFA phase against the chain.
|
|
2484
2582
|
const tabulatedScene = chart !== undefined || callerScene?.kind === "material-function";
|
|
2485
2583
|
// #391: a DNG from region parts takes the whole frame's RAW from the parts instead of rendering it.
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
2584
|
+
// #571: simulateCaptureFromParts takes the RAW from the parts in the same way, then develops it as a capture does.
|
|
2585
|
+
const dngParts = run === "dng-from-parts" || run === "develop-from-parts" ? viewOption : undefined, partsCaller = run === "dng-from-parts" ? "serializeSimulatedCaptureDngFromParts" : "simulateCaptureFromParts";
|
|
2586
|
+
if (dngParts !== undefined && regionNative)
|
|
2587
|
+
fail(partsCaller + " takes the whole frame: the options take no output.region.");
|
|
2489
2588
|
let tabulated;
|
|
2490
2589
|
// #568: lens.catalogue's fast method: tabulated expected electrons with the PSF blended over a field grid (catalogue-lens-signal.ts), for a
|
|
2491
2590
|
// still camera, site-centre queries and a chart or material scene; verified per CFA phase against the exact chain within its tolerance.
|
|
@@ -2544,12 +2643,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2544
2643
|
electrons: createDisplayViewExpectedSignal(signalInput), onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
2545
2644
|
}
|
|
2546
2645
|
let rawOutput;
|
|
2547
|
-
if (dngParts !== undefined)
|
|
2548
|
-
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight), providerEvaluationCount: 0 };
|
|
2646
|
+
if (dngParts !== undefined) {
|
|
2647
|
+
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight, partsCaller, fastReport.requested), providerEvaluationCount: 0 };
|
|
2648
|
+
if (run === "develop-from-parts")
|
|
2649
|
+
assume("parts", "model", { partCount: dngParts.length }, "The whole render's RAW, joined from region renders of this request (#571); how they were rendered is in their results.");
|
|
2650
|
+
}
|
|
2549
2651
|
else if (tabulated && catalogueFastRun) {
|
|
2550
2652
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2551
|
-
|
|
2552
|
-
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
2653
|
+
markFast("lens.catalogue");
|
|
2553
2654
|
for (const a of lensKit.catalogueFastAssumptions(catalogueFastRun))
|
|
2554
2655
|
assume(a.id, "model", a.value, a.explanation);
|
|
2555
2656
|
}
|
|
@@ -2633,16 +2734,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2633
2734
|
return [channel, { conversionGainElectronsPerCode: resolved.regime.systemConversionGainElectronsPerCode.value / (resolved.regime.adc.digitalGain ?? 1),
|
|
2634
2735
|
readNoiseRmsElectrons: resolved.combinedReadNoiseRmsElectrons }];
|
|
2635
2736
|
}));
|
|
2636
|
-
const dngStarted = clockMs();
|
|
2737
|
+
const dngStarted = clockMs(), opcodes = dngOpcodeSelection && lensKit.createDngLensCorrectionOpcodes(lensKit.resolveGenericLens({ lensId: catalogue.lensId, activeImagingArea: catalogueFrame, fNumber }), dngOpcodeSelection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
2637
2738
|
dng = await serializeNativeRawDng({ raw: rawOutput.raw, colorProfile, noise, ...(dngCapturedAtUtc === undefined ? {} : { capturedAtUtc: dngCapturedAtUtc }),
|
|
2638
|
-
...(textMetadata ? { textMetadata } : {}) });
|
|
2739
|
+
...(textMetadata ? { textMetadata } : {}), ...(opcodes ? { opcodeList3: opcodes.opcodeList3 } : {}) });
|
|
2740
|
+
if (opcodes)
|
|
2741
|
+
assume("output.dng.lensCorrectionOpcodes", "model", { tag: 51022, opcodes: opcodes.opcodes }, "output.lensCorrections' profile as optional DNG opcodes, in order (docs/NATIVE_RAW_AND_PRINT.md, \"Lens-correction opcodes\"); the RAW is unchanged.");
|
|
2639
2742
|
encodeMs = clockMs() - dngStarted;
|
|
2640
2743
|
assumeTextMetadata(TEXT_METADATA_LAYOUT.dng);
|
|
2641
2744
|
checkAbort();
|
|
2642
2745
|
assume("output.dng", "model", { writer: "serializeNativeRawDng", schemaVersion: dng.schemaVersion, whiteLevel: dng.whiteLevel, blackLevels: dng.blackLevels,
|
|
2643
2746
|
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.");
|
|
2644
2747
|
}
|
|
2645
|
-
if (
|
|
2748
|
+
if (run === "dng-from-parts")
|
|
2646
2749
|
return dng;
|
|
2647
2750
|
// Development
|
|
2648
2751
|
const development = { phaseProfiles, colorProfile,
|
|
@@ -2675,60 +2778,94 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2675
2778
|
"Below f_c the gain is bounded, so attenuated detail is only partly restored and noise is amplified by up to the measured responsePeakGain. " +
|
|
2676
2779
|
"Not inverted: the pixel aperture, optical anti-aliasing filter, demosaic blur, defocus and aberrations. One design wavelength for all channels (the simulated blur is polychromatic).");
|
|
2677
2780
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2781
|
+
// #570: the lens corrections after the diffraction correction (which acts on the sensor's own lattice), before the tone curve.
|
|
2782
|
+
let cornerStops = null;
|
|
2783
|
+
if (lensCorrection) {
|
|
2784
|
+
const s = lensCorrectionSelection, c = lensCorrection, k = (channel) => c.scales[channel];
|
|
2785
|
+
if (s.vignetting)
|
|
2786
|
+
cornerStops = lensKit.resolveGenericLens({ lensId: catalogue.lensId, activeImagingArea: catalogueFrame, fNumber }).character.vignetting.cornerStops;
|
|
2787
|
+
assume("output.lensCorrections", "model", { profileId: "photivra-" + catalogue.lensId + ":corrections", ...s, k1: c.k1, channelScales: { red: k(0), green: k(1), blue: k(2) },
|
|
2788
|
+
cornerStops, cornerGain: cornerStops === null ? 1 : 2 ** cornerStops, magnification: 1 / c.fill, ...(imageRegion ? { developedRect: region } : {}), dng: "uncorrected" }, "The catalogue lens's correction profile (createGenericLensCorrectionProfile; an approximation) on the developed linear colour: pixel p reads channel c bilinearly at q(1 + k1|q|²/R²), q = a_c·p/magnification " +
|
|
2789
|
+
"(the smallest magnification ≥ 1 that keeps every source in the frame), times det J/E at the source, so a flat field stays flat (E = (1 − bρ²)², 1/det J the distortion's area factor). " +
|
|
2790
|
+
"Corners lose resolution, corner noise rises with the gain, axial colour stays. The DNG is not corrected.");
|
|
2791
|
+
}
|
|
2792
|
+
// Row progress of a development stage, yielding to the host about every 8 ms. #571: the detail stages run row by row in place
|
|
2793
|
+
// (one developed image in memory) and report each output row.
|
|
2794
|
+
const rowProgress = (stage) => {
|
|
2795
|
+
let lastYield = Date.now();
|
|
2796
|
+
return async (completed, total) => {
|
|
2797
|
+
onProgress?.({ stage, completedTileCount: completed, totalTileCount: total });
|
|
2798
|
+
if (Date.now() - lastYield >= 8) {
|
|
2799
|
+
await yieldControl();
|
|
2800
|
+
checkAbort();
|
|
2801
|
+
lastYield = Date.now();
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
};
|
|
2805
|
+
const detailRows = async (stage, rows) => {
|
|
2806
|
+
const tick = rowProgress(stage);
|
|
2807
|
+
let step = rows.next();
|
|
2808
|
+
for (; !step.done; step = rows.next())
|
|
2809
|
+
await tick(step.value, outputHeight);
|
|
2810
|
+
return step.value;
|
|
2811
|
+
};
|
|
2812
|
+
const correctLinear = async (rgb) => {
|
|
2813
|
+
if (diffractionFilter) {
|
|
2814
|
+
const corrected = await detailRows("diffraction-correction", correctDiffractionInPlace(rgb, outputWidth, outputHeight, diffractionFilter));
|
|
2815
|
+
detailDiagnostics.diffractionCorrection = { ...corrected, cutoffCyclesPerPixel: diffractionFilter.cutoffCyclesPerPixel,
|
|
2816
|
+
responsePeakGain: diffractionFilter.responsePeakGain, responseMaximumBeyondCutoff: diffractionFilter.responseMaximumBeyondCutoff };
|
|
2817
|
+
}
|
|
2818
|
+
if (lensCorrection) {
|
|
2819
|
+
const into = imageRegion ?? { x: 0, y: 0, width, height };
|
|
2820
|
+
rgb = lensKit.applyDevelopedLensCorrection(lensCorrection, rgb, region ?? into, into, cornerStops);
|
|
2821
|
+
outputWidth = into.width;
|
|
2822
|
+
outputHeight = into.height;
|
|
2823
|
+
}
|
|
2824
|
+
return rgb;
|
|
2685
2825
|
};
|
|
2686
2826
|
if (sharpening)
|
|
2687
2827
|
assume("output.sharpening", "model", { model: "development-detail", modelVersion: DEVELOPMENT_DETAIL_MODEL_VERSION, method: "unsharp-mask", ...sharpening,
|
|
2688
2828
|
kernelRadiusPixels: Math.ceil(3 * sharpening.radiusPixels), domain: "srgb-encoded", stage: "after the tone curve, gamut handling and sRGB transfer; before quantization", edgeHandling: "clamp" }, "Unsharp mask y = x + amount·(x − G_σ∗x) where |x − G_σ∗x| ≥ threshold (the GIMP and ImageMagick unsharp mask: radius, amount, threshold), per channel on the sRGB-encoded values in [0, 1], " +
|
|
2689
2829
|
"separable Gaussian truncated at ⌈3σ⌉ with clamped edges, then clipped to [0, 1] and rounded. It raises contrast at edges and creates halos (overshoot and undershoot; see diagnostics.detail.sharpening) " +
|
|
2690
2830
|
"and sharpens noise above the threshold. It adds no optical detail.");
|
|
2691
|
-
if (detailRequested)
|
|
2831
|
+
if (detailRequested || lensCorrection)
|
|
2692
2832
|
assume("output.developmentOrder", "model", [...(exposureGain === 1 ? [] : ["development exposure push (RAW)"]), "demosaic", "colour and white balance", ...(diffractionCorrectionOption ? ["diffraction correction (linear)"] : []),
|
|
2833
|
+
...(lensCorrection ? ["lens corrections (linear)"] : []),
|
|
2693
2834
|
"rendering exposure and tone curve", "gamut handling", "transfer function", ...(sharpening ? ["unsharp mask"] : []), "quantization"], "Development detail order: the deconvolution acts on linear light, where the blur is a convolution; the unsharp mask acts on the encoded values just before quantization." +
|
|
2694
2835
|
(exposureGain === 1 ? "" : " A development exposure push (output.exposureCompensationStops) scales the black-subtracted, white-normalized RAW first, so pushed sites clip at white before the deconvolution sees them."));
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2836
|
+
// Wide gamut: linear sRGB/D65 → target primaries (perceptual gamut mapping when asked), encoded.
|
|
2837
|
+
const encodeLinear = (rgb, highlight) => {
|
|
2838
|
+
const mapping = perceptual ? prepareRgbGamutMapping({ colorSpace: colorEncoding, method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION }) : undefined;
|
|
2839
|
+
const encoded = encodeSdrFromLinear(rgb, colorEncoding, pngBitDepth, mapping);
|
|
2840
|
+
if (mapping)
|
|
2841
|
+
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2842
|
+
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2843
|
+
return { raw: rawOutput.raw, integerSamples: encoded.samples, highlight, diagnostics: encoded.diagnostics };
|
|
2844
|
+
};
|
|
2845
|
+
if (ahd || ((detailRequested || lensCorrection) && !linearPath)) {
|
|
2846
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"), ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined);
|
|
2847
|
+
const rgb = await correctLinear(linear.rgb);
|
|
2848
|
+
if (!linearPath) {
|
|
2849
|
+
let codes, counts;
|
|
2850
|
+
if (sharpening) {
|
|
2851
|
+
const rendered = await detailRows("sharpening", renderSdrWithUnsharpMaskInPlace(rgb, outputWidth, outputHeight, rendering, sharpening));
|
|
2852
|
+
detailDiagnostics.sharpening = rendered.sharpening;
|
|
2853
|
+
({ codes, counts } = rendered);
|
|
2854
|
+
}
|
|
2855
|
+
else {
|
|
2856
|
+
// The native development's SDR kernel, row by row, on the AHD-developed linear sRGB.
|
|
2857
|
+
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
2858
|
+
codes = rendering.bitDepth === 8 ? new Uint8Array(outputWidth * outputHeight * 3) : new Uint16Array(outputWidth * outputHeight * 3);
|
|
2859
|
+
counts = { negative: 0, above: 0, changed: 0, low: 0, high: 0, outOfGamut: false };
|
|
2860
|
+
for (let y = 0; y < outputHeight; y++)
|
|
2861
|
+
renderSdrRunInto(rgb, y * outputWidth * 3, outputWidth, rendering, tone, 2 ** rendering.renderingExposureEv, maximum, codes, y * outputWidth * 3, scratch, counts);
|
|
2703
2862
|
}
|
|
2704
|
-
}, ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined);
|
|
2705
|
-
const rgb = correctLinear(linear.rgb);
|
|
2706
|
-
if (!linearPath && sharpening) {
|
|
2707
|
-
const rendered = renderSdrWithUnsharpMask(rgb, outputWidth, outputHeight, rendering, sharpening);
|
|
2708
|
-
detailDiagnostics.sharpening = rendered.sharpening;
|
|
2709
|
-
developed = { raw: rawOutput.raw, integerSamples: rendered.codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: rendered.counts.negative,
|
|
2710
|
-
renderingAboveReferenceSampleCount: rendered.counts.above, toneChangedSampleCount: rendered.counts.changed, gamutClippedLowSampleCount: rendered.counts.low,
|
|
2711
|
-
gamutClippedHighSampleCount: rendered.counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
2712
|
-
}
|
|
2713
|
-
else if (!linearPath) {
|
|
2714
|
-
// The native development's SDR kernel, row by row, on the AHD-developed linear sRGB.
|
|
2715
|
-
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
2716
|
-
const codes = rendering.bitDepth === 8 ? new Uint8Array(outputWidth * outputHeight * 3) : new Uint16Array(outputWidth * outputHeight * 3);
|
|
2717
|
-
const counts = { negative: 0, above: 0, changed: 0, low: 0, high: 0, outOfGamut: false };
|
|
2718
|
-
for (let y = 0; y < outputHeight; y++)
|
|
2719
|
-
renderSdrRunInto(rgb, y * outputWidth * 3, outputWidth, rendering, tone, 2 ** rendering.renderingExposureEv, maximum, codes, y * outputWidth * 3, scratch, counts);
|
|
2720
2863
|
developed = { raw: rawOutput.raw, integerSamples: codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: counts.negative,
|
|
2721
2864
|
renderingAboveReferenceSampleCount: counts.above, toneChangedSampleCount: counts.changed, gamutClippedLowSampleCount: counts.low,
|
|
2722
2865
|
gamutClippedHighSampleCount: counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
2723
2866
|
}
|
|
2724
|
-
else
|
|
2725
|
-
|
|
2726
|
-
const encoded = encodeSdrFromLinear(rgb, colorEncoding, pngBitDepth, mapping);
|
|
2727
|
-
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
2728
|
-
if (mapping)
|
|
2729
|
-
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2730
|
-
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2731
|
-
}
|
|
2867
|
+
else
|
|
2868
|
+
developed = encodeLinear(rgb, linear.highlight);
|
|
2732
2869
|
}
|
|
2733
2870
|
else if (!linearPath) {
|
|
2734
2871
|
const devTask = createNativeRawDevelopmentTask({ ...development, raw: rawOutput.raw }, { yieldControl,
|
|
@@ -2738,22 +2875,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2738
2875
|
}
|
|
2739
2876
|
else {
|
|
2740
2877
|
// Wide gamut: the native development operator up to linear sRGB/D65 (unclipped), then XYZ → target primaries.
|
|
2741
|
-
|
|
2742
|
-
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, async (completed, total) => {
|
|
2743
|
-
onProgress?.({ stage: "development", completedTileCount: completed, totalTileCount: total });
|
|
2744
|
-
if (Date.now() - lastYield >= 8) {
|
|
2745
|
-
await yieldControl();
|
|
2746
|
-
checkAbort();
|
|
2747
|
-
lastYield = Date.now();
|
|
2748
|
-
}
|
|
2749
|
-
});
|
|
2878
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"));
|
|
2750
2879
|
{
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
2754
|
-
if (mapping) {
|
|
2755
|
-
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2756
|
-
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2880
|
+
developed = encodeLinear(await correctLinear(linear.rgb), linear.highlight);
|
|
2881
|
+
if (perceptual) {
|
|
2757
2882
|
assume("output.gamutMapping", "model", { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION, colorSpace: colorEncoding }, "prepareRgbGamutMapping: CIELAB L* and hue held, chroma above 90 % of the boundary chroma C_max(L*, h) compressed with a tanh knee so it never reaches C_max (Morovič 2008, §10). In-gamut colours below the knee are unchanged; saturated in-gamut colours above it are desaturated slightly. L* ≥ 100 (above the RAW white) maps to white. CIELAB hue is not perceptually constant for saturated blues, so they can shift toward purple.");
|
|
2758
2883
|
}
|
|
2759
2884
|
assume("output.colorEncoding.conversion", "model", { from: "linear-srgb-d65 (unclipped)", via: "CIE XYZ", to: colorEncoding, gamutHandling: perceptual ? "cielab-hue-preserving-chroma-compression" : "clip-components",
|
|
@@ -2812,17 +2937,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2812
2937
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
2813
2938
|
...(highlightMode === "reconstruct" ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
2814
2939
|
...(perceptual ? ["output.gamutMapping.perceptual"] : []), ...(diffractionFilter ? ["output.diffractionCorrection"] : []),
|
|
2815
|
-
...(sharpening ? ["output.sharpening"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
2940
|
+
...(sharpening ? ["output.sharpening"] : []), ...(lensCorrection ? ["output.lensCorrections"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
2816
2941
|
// #552: the detail stages' contracts, in execution order inside the development.
|
|
2817
|
-
const detailContracts = diffractionFilter ? ["designDiffractionCorrectionFilter", "applyDiffractionCorrection"] : [], sdrContract = sharpening ? "renderSdrWithUnsharpMask" : "SDR rendering kernel";
|
|
2942
|
+
const detailContracts = [...(diffractionFilter ? ["designDiffractionCorrectionFilter", "applyDiffractionCorrection"] : []), ...(lensCorrection ? ["createGenericLensCorrectionProfile (developed linear colour)"] : [])], sdrContract = sharpening ? "renderSdrWithUnsharpMask" : "SDR rendering kernel";
|
|
2818
2943
|
return Object.freeze({
|
|
2819
2944
|
facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
2820
2945
|
image: { width: outputWidth, height: outputHeight, format, samples: developed.integerSamples, ...(jpeg ? { jpeg } : {}), ...(png ? { png, colorEncoding: colorEncoding } : {}), ...(dng ? { dng: dng.bytes } : {}) },
|
|
2821
2946
|
raw: developed.raw, plan: developmentPlan,
|
|
2822
|
-
...(region && regionNative ? { region: { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2947
|
+
...(region && regionNative ? { region: { ...imageRegion ?? region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2823
2948
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2824
2949
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" :
|
|
2825
2950
|
defocusImageSpace ? "image-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
2951
|
+
sensor,
|
|
2826
2952
|
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds), ...isoReport,
|
|
2827
2953
|
metering, meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, standardExposureDeviationStops: exposureDeviationStops,
|
|
2828
2954
|
standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, resolution },
|
|
@@ -2846,7 +2972,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2846
2972
|
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
2847
2973
|
...(ahd ? ["native RAW development operator (colour, after AHD)", ...detailContracts, ...(linearPath ? ["resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : [sdrContract])] :
|
|
2848
2974
|
linearPath ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, "resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] :
|
|
2849
|
-
detailRequested ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2975
|
+
detailRequested || lensCorrection ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2850
2976
|
...(jpeg ? ["createPrintJpegTask"] : []), ...(png ? ["serializePngImage"] : []), ...(dng ? ["resolveSensorReadoutRegime", "serializeNativeRawDng"] : [])],
|
|
2851
2977
|
limitations: ["Educational approximation: synthetic tier sensor values, idealized colorimetric sensor, planar Lambertian scene and point optics; not a calibrated camera emulation.",
|
|
2852
2978
|
"Not offered by the facade: atmospheric haze and polarizers (the scene-radiance provider contract declares no volumetric transport or polarization), flash, video, HDR brackets and processing operators; compose those APIs directly."] },
|