@photivra/engine 2.17.0 → 2.19.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 +116 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/camera/sensor-equivalence.d.ts +156 -0
- package/dist/camera/sensor-equivalence.js +208 -0
- package/dist/camera/sensor-equivalence.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 +1 -1
- 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-environment-tabulated.d.ts +5 -0
- package/dist/capture/native-environment-tabulated.js +1 -1
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/release-sequence.js +2 -47
- package/dist/capture/release-sequence.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 +33 -0
- package/dist/core/shared-helpers.js +34 -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/ideal-lens.d.ts +25 -0
- package/dist/equipment/ideal-lens.js +21 -0
- package/dist/equipment/ideal-lens.js.map +1 -0
- 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/catalogue-lens-signal.d.ts +94 -0
- package/dist/facade/catalogue-lens-signal.js +362 -0
- package/dist/facade/catalogue-lens-signal.js.map +1 -0
- package/dist/facade/defocus-image-space-provider.js +5 -2
- package/dist/facade/defocus-image-space-provider.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-lens-signal.d.ts +32 -1
- package/dist/facade/display-view-lens-signal.js +85 -10
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +8 -0
- package/dist/facade/expected-signal-setup.js +7 -0
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +5 -0
- package/dist/facade/expected-signal.js +5 -1
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +5 -2
- package/dist/facade/generic-lens-capture.js +5 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +67 -8
- package/dist/facade/simulate-capture.js +262 -126
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +1 -1
- 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 +6 -1
- package/dist/index.js +5 -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/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/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-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.js +4 -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/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 +2 -6
- package/dist/output/generic-lens-corrections.js +3 -6
- 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/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/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 -35
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +5 -0
- package/dist/sensor/exposure-window.js +7 -22
- 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 +3 -57
- 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 +3 -29
- 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 -108
- 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/spatial-sample-reduction.js +2 -19
- 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 +2 -35
- 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/stabilization/camera-shake.js +1 -6
- package/dist/stabilization/camera-shake.js.map +1 -1
- package/dist/stabilization/system.js +2 -80
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +69 -2
- package/docs/QUICK_START.md +136 -6
- package/docs/USAGE.md +89 -3
- package/docs/api/camera-sensor-equivalence.md +209 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-ideal-lens.md +26 -0
- package/docs/api/exports.json +118 -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-simulate-capture.md +62 -8
- 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-developed-lens-corrections.md +12 -0
- package/docs/api/output-generic-lens-corrections.md +0 -9
- package/llms.txt +3 -0
- package/package.json +1 -1
|
@@ -82,6 +82,7 @@ import { DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES, siteKernelRuns } from "../o
|
|
|
82
82
|
import { createDefocusBokehSiteKernel, resolveDefocusBokehOptions } from "../optics/defocus-bokeh-kernel.js";
|
|
83
83
|
import { planDefocusBokehZones } from "./defocus-bokeh-zones.js";
|
|
84
84
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
85
|
+
import { IDEAL_LENS_LIMITS } from "../equipment/ideal-lens.js";
|
|
85
86
|
import { calculateChannelElectronsPerRadiance } from "./channel-response.js";
|
|
86
87
|
import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
|
|
87
88
|
import { prepareSensorColorDevelopment } from "../color/sensor-color-development.js";
|
|
@@ -89,7 +90,7 @@ import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
|
89
90
|
import { multiplyMatrices } from "../color/colorimetry-math.js";
|
|
90
91
|
import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
91
92
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
92
|
-
import {
|
|
93
|
+
import { correctDiffractionInPlace, defaultDiffractionCorrectionRadiusPixels, designDiffractionCorrectionFilter, parseDiffractionCorrectionSettings, parseUnsharpMaskSettings, renderSdrWithUnsharpMaskInPlace, DEVELOPMENT_DETAIL_MODEL_VERSION } from "../processing/development-detail.js";
|
|
93
94
|
import { parseSensorReadoutConversionProfile, resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
94
95
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
95
96
|
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
@@ -106,13 +107,13 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
106
107
|
export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
107
108
|
"lens.diffraction", "lens.defocus", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
108
109
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
109
|
-
"output.diffractionCorrection", "output.sharpening", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
110
|
+
"output.diffractionCorrection", "output.sharpening", "output.lensCorrections", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
110
111
|
/**
|
|
111
112
|
* The options that take the exact-or-fast flag (#395), in the path style of {@link SIMULATE_CAPTURE_COST_OPTIONS}: each accepts
|
|
112
113
|
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
113
114
|
* whether or not it has a fast method yet; one without runs exact and reports `false`.
|
|
114
115
|
*/
|
|
115
|
-
export const SIMULATE_CAPTURE_FAST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
116
|
+
export const SIMULATE_CAPTURE_FAST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.diffraction", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "output.demosaic.ahd"]);
|
|
116
117
|
export const FACADE_EVIDENCE = /* @__PURE__ */ Object.freeze([/* @__PURE__ */ Object.freeze({ sourceOrigin: "photivra",
|
|
117
118
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
118
119
|
const DEFAULTS = /* @__PURE__ */ Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
@@ -552,20 +553,20 @@ function resolveAutomaticSlowestShutterOption(value, context) {
|
|
|
552
553
|
"reciprocal rule is a rule of thumb and is not used.");
|
|
553
554
|
return report;
|
|
554
555
|
}
|
|
555
|
-
function defaultProgramLine() {
|
|
556
|
+
function defaultProgramLine(wide, narrow, anchor) {
|
|
556
557
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
557
558
|
// it first opens the aperture toward f/4, then lengthens the shutter. For more light it first closes toward
|
|
558
|
-
// f/16, then shortens the shutter.
|
|
559
|
-
|
|
559
|
+
// 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
|
|
560
|
+
// 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
|
|
561
|
+
// (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.
|
|
562
|
+
const own = wide === 4 && narrow === 16;
|
|
563
|
+
return parseExposureProgramLineProfile({ schemaVersion: "0.1.0", profileId: "facade-generic-program-line" + (own ? "" : "-lens-range"), profileVersion: "1.0.0",
|
|
560
564
|
scientificStatus: "approximation", policyKind: "generic-program-line", interpolation: "log2-aperture-shutter", evidence: FACADE_EVIDENCE,
|
|
561
565
|
limitations: ["Generic educational program line authored for the capture facade; not any manufacturer's program."],
|
|
562
|
-
nodes: [
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
{ nodeId: "plus-2", opticalExposureStopsFromReference: 2, aperture: 4, shutterSeconds: 1 / 125 },
|
|
567
|
-
{ nodeId: "plus-9", opticalExposureStopsFromReference: 9, aperture: 4, shutterSeconds: 128 / 125 }
|
|
568
|
-
] });
|
|
566
|
+
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]] :
|
|
567
|
+
[[narrow, 1 / 2000], [narrow, 1 / 125], [wide, 1 / 125], [wide, 128 / 125]].filter((_, i) => i !== 2 || wide < narrow)
|
|
568
|
+
.map(([aperture, shutterSeconds], i) => ["n" + i, Math.log2(shutterSeconds * anchor.aperture ** 2 / anchor.shutterSeconds / aperture ** 2), aperture, shutterSeconds]))
|
|
569
|
+
.map(([nodeId, opticalExposureStopsFromReference, aperture, shutterSeconds]) => ({ nodeId, opticalExposureStopsFromReference, aperture, shutterSeconds })) });
|
|
569
570
|
}
|
|
570
571
|
/** PNG colour signalling: the sRGB chunk for sRGB, ITU-T H.273 code points through cICP otherwise. */
|
|
571
572
|
function pngColourEncoding(encoding) {
|
|
@@ -743,13 +744,13 @@ export async function serializeSimulatedCaptureDngFromParts(options, parts) {
|
|
|
743
744
|
return await runCapture(options, "dng-from-parts", parts);
|
|
744
745
|
}
|
|
745
746
|
/** The whole frame's packed RAW from region parts (see `serializeSimulatedCaptureDngFromParts`). */
|
|
746
|
-
function joinNativeRawParts(value, plan, width, height) {
|
|
747
|
+
function joinNativeRawParts(value, plan, width, height, caller) {
|
|
747
748
|
if (!Array.isArray(value) || value.length === 0)
|
|
748
|
-
throw new InvalidConfigurationError("
|
|
749
|
+
throw new InvalidConfigurationError(caller + " needs at least one part.");
|
|
749
750
|
const parts = value, count = width * height;
|
|
750
751
|
const codes = new Uint16Array(count), blackLevels = new Uint16Array(count), digitalSaturationCodes = new Uint16Array(count), saturationFlags = new Uint8Array(count);
|
|
751
752
|
const covered = new Uint8Array(count), white = new Uint16Array(4);
|
|
752
|
-
const bad = (k, what) => { throw new InvalidConfigurationError("
|
|
753
|
+
const bad = (k, what) => { throw new InvalidConfigurationError("Part " + k + ": " + what); };
|
|
753
754
|
parts.forEach((part, k) => {
|
|
754
755
|
const rect = part?.region?.nativeRect, raw = part?.raw;
|
|
755
756
|
if (!rect || !raw)
|
|
@@ -791,9 +792,9 @@ function joinNativeRawParts(value, plan, width, height) {
|
|
|
791
792
|
}
|
|
792
793
|
});
|
|
793
794
|
if (covered.includes(0))
|
|
794
|
-
throw new InvalidConfigurationError("The
|
|
795
|
+
throw new InvalidConfigurationError("The parts do not cover every native site of the frame.");
|
|
795
796
|
if (white.includes(0))
|
|
796
|
-
throw new InvalidConfigurationError("The
|
|
797
|
+
throw new InvalidConfigurationError("The parts lack a white level for a CFA phase.");
|
|
797
798
|
return { plan, codes, blackLevels, digitalSaturationCodes, saturationFlags, effectiveSaturationCodes: white };
|
|
798
799
|
}
|
|
799
800
|
/**
|
|
@@ -959,35 +960,65 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
959
960
|
const body = preset.body, lens = preset.lens;
|
|
960
961
|
// Lens
|
|
961
962
|
const lensOptions = allowKeys(top.lens, "lens", ["focalLengthMm", "fNumber", "focusDistanceM", "pupilSampling", "projection", "distortion", "vignetting", "psf", "diffraction", "prime", "defocus",
|
|
962
|
-
"catalogue", "focusScatter"]);
|
|
963
|
+
"catalogue", "focusScatter", "ideal"]);
|
|
963
964
|
// Generic prime (2.3): resolved on the simulated frame's size, which the output and sensor sections validate again below.
|
|
964
965
|
let prime;
|
|
966
|
+
// The simulated frame in mm (output width × height × pixel pitch), validated again below.
|
|
967
|
+
const frameMm = () => {
|
|
968
|
+
const o = (typeof top.output === "object" && top.output !== null ? top.output : {}), sn = (typeof top.sensor === "object" && top.sensor !== null ? top.sensor : {});
|
|
969
|
+
const frameW = positive(o.width ?? DEFAULTS.width, "output.width"), frameH = positive(o.height ?? DEFAULTS.height, "output.height"), framePitch = positive(sn.pixelPitchMicrometers ?? DEFAULTS.pixelPitchMicrometers, "sensor.pixelPitchMicrometers");
|
|
970
|
+
return { widthMm: frameW * framePitch / 1000, heightMm: frameH * framePitch / 1000 };
|
|
971
|
+
};
|
|
965
972
|
if (lensOptions.prime !== undefined) {
|
|
966
973
|
if (lensOptions.focalLengthMm !== undefined)
|
|
967
974
|
fail("lens.prime and lens.focalLengthMm cannot both be given: the prime sets the focal length.");
|
|
968
|
-
const
|
|
969
|
-
const frameW = positive(o.width ?? DEFAULTS.width, "output.width"), frameH = positive(o.height ?? DEFAULTS.height, "output.height"), framePitch = positive(sn.pixelPitchMicrometers ?? DEFAULTS.pixelPitchMicrometers, "sensor.pixelPitchMicrometers");
|
|
975
|
+
const activeImagingArea = frameMm();
|
|
970
976
|
if (typeof lensOptions.prime !== "number")
|
|
971
977
|
fail("lens.prime must be a generic prime's full-frame-equivalent focal length in mm.");
|
|
972
|
-
prime = resolveGenericPrimeLens({ equivalentFocalLengthMm: lensOptions.prime, activeImagingArea
|
|
978
|
+
prime = resolveGenericPrimeLens({ equivalentFocalLengthMm: lensOptions.prime, activeImagingArea });
|
|
973
979
|
assume("lens.prime", "model", { lensId: prime.lensId, equivalentFocalLengthMm: prime.equivalentFocalLengthMm, focalLengthMm: prime.focalLengthMm, cropFactor35Mm: prime.cropFactor35Mm,
|
|
974
980
|
widestFNumber: prime.widestFNumber, narrowestFNumber: prime.narrowestFNumber, minimumFocusDistanceM: prime.minimumFocusDistanceM }, "Generic prime (not a real product): focal length = equivalent ÷ the simulated frame's diagonal crop factor, f/" + prime.widestFNumber + "–f/" + prime.narrowestFNumber +
|
|
975
981
|
", minimum focus max(0.10 m, f·(1 + 1/0.15)) (thin-lens approximation). It replaces the tier lens's focal length and f-number range; the tier lens's transmission is kept.");
|
|
976
982
|
}
|
|
983
|
+
// Ideal lens (#576): a physical focal length on any frame, every f-number within IDEAL_LENS_LIMITS.
|
|
984
|
+
let ideal;
|
|
985
|
+
if (lensOptions.ideal !== undefined) {
|
|
986
|
+
for (const key of ["prime", "catalogue", "focalLengthMm"])
|
|
987
|
+
if (lensOptions[key] !== undefined)
|
|
988
|
+
fail("lens.ideal cannot be combined with lens." + key + ".");
|
|
989
|
+
const io = allowKeys(lensOptions.ideal, "lens.ideal", ["focalLengthMm", "widestFNumber", "narrowestFNumber"]), f = io.focalLengthMm, { focalLengthMm: b, fNumber: n } = IDEAL_LENS_LIMITS;
|
|
990
|
+
if (!(typeof f === "number" && f >= b.minimum && f <= b.maximum))
|
|
991
|
+
fail("lens.ideal.focalLengthMm must be " + b.minimum + "–" + b.maximum + " mm (IDEAL_LENS_LIMITS).");
|
|
992
|
+
// #576 (owner, 2026-11-09): an optional f-number range within IDEAL_LENS_LIMITS, for the automatic exposure modes; default the whole range.
|
|
993
|
+
const ranged = io.widestFNumber !== undefined || io.narrowestFNumber !== undefined, lo = io.widestFNumber === undefined ? n.minimum : io.widestFNumber, hi = io.narrowestFNumber === undefined ? n.maximum : io.narrowestFNumber;
|
|
994
|
+
if (!(typeof lo === "number" && typeof hi === "number" && lo >= n.minimum && hi <= n.maximum && lo <= hi))
|
|
995
|
+
fail("lens.ideal.widestFNumber and narrowestFNumber must be f-numbers in f/" + n.minimum + "–f/" + n.maximum + " (IDEAL_LENS_LIMITS), the widest no larger than the narrowest.");
|
|
996
|
+
const lensId = "ideal-" + f + "mm";
|
|
997
|
+
ideal = { id: lensId, name: lensId, focalLengthMm: f, widestFNumber: lo, narrowestFNumber: hi };
|
|
998
|
+
assume("lens.ideal", "model", { lensId, focalLengthMm: f, ...(ranged ? { widestFNumber: lo, narrowestFNumber: hi } : {}) }, "Ideal thin lens without aberrations (not a real product) at this physical focal length" +
|
|
999
|
+
(ranged ? ", f/" + lo + "–f/" + hi : "") + "; the tier lens's transmission is kept.");
|
|
1000
|
+
}
|
|
977
1001
|
// Generic lens catalogue (#549): resolved on the simulated frame like the prime; its character is built once the f-number is known.
|
|
978
1002
|
let catalogue, catalogueFrame;
|
|
979
1003
|
let lensKit;
|
|
1004
|
+
// #568: the shorthand id or the record { kind: <id>, fast }.
|
|
1005
|
+
let catalogueId, catalogueFast;
|
|
1006
|
+
if (lensOptions.catalogue !== undefined && typeof lensOptions.catalogue === "object" && lensOptions.catalogue !== null) {
|
|
1007
|
+
const record = allowKeys(lensOptions.catalogue, "lens.catalogue", ["kind", "fast"]);
|
|
1008
|
+
if (typeof record.kind !== "string")
|
|
1009
|
+
fail("lens.catalogue.kind must be a catalogue lens id (GENERIC_LENS_IDS).");
|
|
1010
|
+
catalogueId = record.kind;
|
|
1011
|
+
catalogueFast = fastOverride(record, "lens.catalogue");
|
|
1012
|
+
}
|
|
1013
|
+
else if (lensOptions.catalogue !== undefined)
|
|
1014
|
+
catalogueId = lensOptions.catalogue;
|
|
980
1015
|
if (lensOptions.catalogue !== undefined) {
|
|
981
1016
|
for (const key of ["prime", "focalLengthMm", "projection", "distortion", "vignetting", "psf", "diffraction", "pupilSampling"])
|
|
982
1017
|
if (lensOptions[key] !== undefined)
|
|
983
1018
|
fail("lens.catalogue cannot be combined with lens." + key + ": the catalogue lens sets its own focal length, projection, distortion, vignetting and PSF (with diffraction).");
|
|
984
|
-
|
|
985
|
-
fail("simulateDisplayView does not support lens.catalogue: the catalogue lens's field-dependent PSF, distortion and vignetting run through the capture's environment quadrature.");
|
|
986
|
-
const o = (typeof top.output === "object" && top.output !== null ? top.output : {}), sn = (typeof top.sensor === "object" && top.sensor !== null ? top.sensor : {});
|
|
987
|
-
const frameW = positive(o.width ?? DEFAULTS.width, "output.width"), frameH = positive(o.height ?? DEFAULTS.height, "output.height"), framePitch = positive(sn.pixelPitchMicrometers ?? DEFAULTS.pixelPitchMicrometers, "sensor.pixelPitchMicrometers");
|
|
988
|
-
catalogueFrame = { widthMm: frameW * framePitch / 1000, heightMm: frameH * framePitch / 1000 };
|
|
1019
|
+
catalogueFrame = frameMm();
|
|
989
1020
|
lensKit = await import("./generic-lens-capture.js");
|
|
990
|
-
catalogue = lensKit.resolveGenericLens({ lensId:
|
|
1021
|
+
catalogue = lensKit.resolveGenericLens({ lensId: catalogueId, activeImagingArea: catalogueFrame });
|
|
991
1022
|
assume("lens.catalogue", "model", { lensId: catalogue.lensId, name: catalogue.name, set: catalogue.set, kind: catalogue.kind, equivalentFocalLengthMm: catalogue.equivalentFocalLengthMm,
|
|
992
1023
|
focalLengthMm: catalogue.focalLengthMm, cropFactor35Mm: catalogue.cropFactor35Mm, widestFNumber: catalogue.widestFNumber, narrowestFNumber: catalogue.narrowestFNumber,
|
|
993
1024
|
minimumFocusDistanceM: catalogue.minimumFocusDistanceM, maximumMagnification: catalogue.maximumMagnification, projection: catalogue.projection, fisheyeImage: catalogue.fisheyeImage }, "Generic catalogue lens " + catalogue.name + " (#549; Photivra's own design, not a real product): focal length = equivalent ÷ the simulated frame's diagonal crop factor, f/" +
|
|
@@ -996,8 +1027,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
996
1027
|
}
|
|
997
1028
|
else if (lensOptions.focusScatter !== undefined)
|
|
998
1029
|
fail("lens.focusScatter needs lens.catalogue: the focus scatter is a catalogue lens's own.");
|
|
999
|
-
const
|
|
1000
|
-
|
|
1030
|
+
const listed = prime ?? catalogue, lensRange = listed ? { id: listed.lensId, name: catalogue ? catalogue.name : listed.lensId, focalLengthMm: listed.focalLengthMm,
|
|
1031
|
+
widestFNumber: listed.widestFNumber, narrowestFNumber: listed.narrowestFNumber } : ideal;
|
|
1001
1032
|
const lensExposure = lensRange ? { ...lens.exposure, profileId: "photivra-" + lensRange.id + ":exposure",
|
|
1002
1033
|
focalLengthMmRange: { ...lens.exposure.focalLengthMmRange, value: { minimum: lensRange.focalLengthMm, maximum: lensRange.focalLengthMm } },
|
|
1003
1034
|
aperture: { ...lens.exposure.aperture, widestAvailableFNumber: { kind: "constant", fNumber: { value: lensRange.widestFNumber, evidence: FACADE_EVIDENCE } },
|
|
@@ -1007,8 +1038,9 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1007
1038
|
positive(pick(lensOptions, "focalLengthMm", "lens.focalLengthMm", focalDefault, "The preset lens's focal length (the generic primes are 50 mm)."), "lens.focalLengthMm");
|
|
1008
1039
|
const capabilities = resolveGenericEquipmentExposureCapabilities({ bodyProfile: body.exposure, lensProfile: lensExposure, selectedFocalLengthMm: focalLengthMm });
|
|
1009
1040
|
const subjectDistanceM = positive(pick(top, "subjectDistanceM", "subjectDistanceM", DEFAULTS.subjectDistanceM, "Scene plane 5 m in front of the camera (the generic tiers' reference focus distance)."), "subjectDistanceM");
|
|
1010
|
-
|
|
1011
|
-
|
|
1041
|
+
// #569: twice the focal length itself is admitted (magnification 1, the catalogue Macro's close focus f·(1 + 1/1)).
|
|
1042
|
+
if (subjectDistanceM * 1000 < focalLengthMm * 2)
|
|
1043
|
+
fail("subjectDistanceM must be at least twice the focal length.");
|
|
1012
1044
|
let focusDistanceM = positive(pick(lensOptions, "focusDistanceM", "lens.focusDistanceM", subjectDistanceM, "Focused on the scene plane."), "lens.focusDistanceM");
|
|
1013
1045
|
if (focusDistanceM * 1000 <= focalLengthMm)
|
|
1014
1046
|
fail("lens.focusDistanceM must exceed the focal length.");
|
|
@@ -1149,7 +1181,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1149
1181
|
const diffractionObjectSpace = diffractionRadiusOption !== undefined && !rotating && !lensFieldMapping && !vignetting;
|
|
1150
1182
|
// Output and sensor
|
|
1151
1183
|
const outputOptions = allowKeys(top.output, "output", ["width", "height", "format", "toneCurve", "toneCurveParameters", "highlightMode", "exposureCompensationStops", "whiteBalance", "jpegQuantizationStep",
|
|
1152
|
-
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening"]);
|
|
1184
|
+
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening", "lensCorrections"]);
|
|
1153
1185
|
// Opt-in large images: a caller-raised native pixel limit. Without it the 4094-pixel sides are unchanged.
|
|
1154
1186
|
const raisedPixels = outputOptions.maximumPixels === undefined ? undefined : integerIn(outputOptions.maximumPixels, "output.maximumPixels", 1, NATIVE_RAW_LIMITS.ceilingPixels);
|
|
1155
1187
|
const maximumSide = raisedPixels === undefined ? 4094 : NATIVE_RAW_LIMITS.maximumDimension;
|
|
@@ -1271,6 +1303,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1271
1303
|
unsupported("output.highlightMode \"reconstruct\"", "the view supports the per-channel highlight clip.");
|
|
1272
1304
|
if (outputOptions.diffractionCorrection !== undefined || outputOptions.sharpening !== undefined)
|
|
1273
1305
|
unsupported("output.diffractionCorrection or output.sharpening", "the development detail stages filter the whole developed capture image.");
|
|
1306
|
+
if (outputOptions.lensCorrections !== undefined)
|
|
1307
|
+
unsupported("output.lensCorrections", "the corrections resample the developed capture image.");
|
|
1274
1308
|
if (defocusBokeh?.catsEye)
|
|
1275
1309
|
unsupported("lens.defocus.catsEye", "the expected-signal tables hold one kernel per spectral node, and the cat's eye varies over the field; " +
|
|
1276
1310
|
"simulateCapture renders it. lens.defocus.blades and lens.defocus.sphericalAberration are supported.");
|
|
@@ -1294,19 +1328,22 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1294
1328
|
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
1295
1329
|
fail("output.demosaic must be bilinear or ahd.");
|
|
1296
1330
|
const ahd = demosaicOption === "ahd";
|
|
1297
|
-
// #395: exact or fast per option.
|
|
1331
|
+
// #395: exact or fast per option. Only lens.catalogue has a fast method (#568); it reports true below when it ran.
|
|
1298
1332
|
if (top.fast !== undefined && typeof top.fast !== "boolean")
|
|
1299
1333
|
fail("fast must be true or false.");
|
|
1300
1334
|
const fastTop = top.fast === true;
|
|
1301
1335
|
const fastInUse = [["lens.diffraction", diffractionRadiusOption !== undefined, diffractionFast],
|
|
1336
|
+
["lens.catalogue", catalogue !== undefined, catalogueFast],
|
|
1302
1337
|
["sensor.antiAliasing", antiAliasing !== undefined, antiAliasingFast], ["sensor.pixelAperture.box", boxAperture, boxFast], ["output.demosaic.ahd", ahd, demosaicFast]];
|
|
1303
1338
|
const fastApplied = {};
|
|
1304
1339
|
for (const [option, used] of fastInUse)
|
|
1305
1340
|
if (used)
|
|
1306
1341
|
fastApplied[option] = false;
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1342
|
+
// #568: the catalogue's fast method, when requested (its record's flag, else the top-level one); whether it runs is decided with the RAW path.
|
|
1343
|
+
const catalogueFastRequested = catalogue !== undefined && (catalogueFast ?? fastTop);
|
|
1344
|
+
let fastReport = Object.freeze({
|
|
1345
|
+
requested: fastTop || [diffractionFast, catalogueFast, antiAliasingFast, boxFast, demosaicFast].some((value) => value === true),
|
|
1346
|
+
applied: Object.freeze({ ...fastApplied }), assurance: "engine-reference"
|
|
1310
1347
|
});
|
|
1311
1348
|
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
1312
1349
|
fail("output.borderHandling must be margin or mirror.");
|
|
@@ -1320,23 +1357,46 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1320
1357
|
fail("output.demosaic \"ahd\" with mirror borders needs an output of at least 3 × 3 pixels.");
|
|
1321
1358
|
// Geometry: the demosaic border (if any) on each side; output crop is the active area.
|
|
1322
1359
|
const nativeWidth = width + 2 * margin, nativeHeight = height + 2 * margin;
|
|
1360
|
+
// Lens corrections (#570): the catalogue lens's corrections on the developed linear colour; all off is no option at all.
|
|
1361
|
+
let lensCorrection, lensCorrectionSelection;
|
|
1362
|
+
if (outputOptions.lensCorrections !== undefined) {
|
|
1363
|
+
const keys = ["distortion", "lateralColor", "vignetting"], o = allowKeys(outputOptions.lensCorrections, "output.lensCorrections", keys), selection = {};
|
|
1364
|
+
for (const key of keys) {
|
|
1365
|
+
if (o[key] !== undefined && typeof o[key] !== "boolean")
|
|
1366
|
+
fail("output.lensCorrections." + key + " must be true or false.");
|
|
1367
|
+
selection[key] = o[key] === true;
|
|
1368
|
+
}
|
|
1369
|
+
if (keys.some((key) => selection[key])) {
|
|
1370
|
+
if (idealReference)
|
|
1371
|
+
fail("simulateIdealReference does not take output.lensCorrections: a reference has no camera development.");
|
|
1372
|
+
if (!catalogue || !lensKit)
|
|
1373
|
+
fail("output.lensCorrections needs lens.catalogue: the corrections are the catalogue lens's own profile.");
|
|
1374
|
+
lensCorrectionSelection = selection;
|
|
1375
|
+
lensCorrection = lensKit.prepareDevelopedLensCorrection(catalogue, selection, width, height, pitch / 1000, lensKit.genericLensDispersion);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1323
1378
|
// Region (2.3): a rectangle of the whole frame. Its native sites are the region plus the demosaic reach on each side
|
|
1324
1379
|
// (clipped to the raster, where mirror borders apply exactly as in the whole render), with the origin rounded down to
|
|
1325
1380
|
// the 2 × 2 CFA repeat so the region raster keeps the frame's CFA phase.
|
|
1326
|
-
let region, regionNative;
|
|
1381
|
+
let region, regionNative, imageRegion;
|
|
1327
1382
|
if (outputOptions.region !== undefined) {
|
|
1328
1383
|
const r = allowKeys(outputOptions.region, "output.region", ["x", "y", "width", "height"]);
|
|
1329
1384
|
const x = integerIn(r.x, "output.region.x", 0, width - 1), y = integerIn(r.y, "output.region.y", 0, height - 1);
|
|
1330
1385
|
region = { x, y, width: integerIn(r.width, "output.region.width", 1, width - x), height: integerIn(r.height, "output.region.height", 1, height - y) };
|
|
1386
|
+
// #570: a corrected region develops the frame pixels its corrected pixels read.
|
|
1387
|
+
if (lensCorrection) {
|
|
1388
|
+
imageRegion = region;
|
|
1389
|
+
region = lensKit.developedLensCorrectionSourceRect(lensCorrection, region);
|
|
1390
|
+
}
|
|
1331
1391
|
const reach = margin > 0 ? margin : ahd ? AHD_MARGIN : builtInDemosaic ? builtInDemosaic.kernelReachSites : MARGIN;
|
|
1332
1392
|
let x0 = Math.max(0, region.x + margin - reach), y0 = Math.max(0, region.y + margin - reach);
|
|
1333
1393
|
x0 -= x0 % 2;
|
|
1334
1394
|
y0 -= y0 % 2;
|
|
1335
1395
|
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 };
|
|
1336
1396
|
}
|
|
1337
|
-
|
|
1397
|
+
let outputWidth = region ? region.width : width, outputHeight = region ? region.height : height;
|
|
1338
1398
|
if (region && regionNative)
|
|
1339
|
-
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), " +
|
|
1399
|
+
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), " +
|
|
1340
1400
|
"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. " +
|
|
1341
1401
|
// 2.6: with lens.diffraction the convolution is limited to the region; the sentence of 2.3–2.5 is kept verbatim otherwise, so
|
|
1342
1402
|
// region results without diffraction stay byte-identical to 2.5.0.
|
|
@@ -1518,9 +1578,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1518
1578
|
fail("exposure.compensationStops must be within ±10.");
|
|
1519
1579
|
if (exposureOptions.compensationStops === undefined)
|
|
1520
1580
|
assume("exposure.compensationStops", "default", 0, "No exposure compensation.");
|
|
1521
|
-
|
|
1581
|
+
// #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
|
|
1582
|
+
// 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
|
|
1583
|
+
// 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),
|
|
1584
|
+
// the anchor takes the shortest shutter instead, a lower N²/t; the meter, the resolvers and the program line all measure from the anchor.
|
|
1585
|
+
// An f-number clamped to the lens's range (#583 limits the program line's f/4 and f/16 with it).
|
|
1586
|
+
const clamp = (n) => lensRange ? Math.min(lensRange.narrowestFNumber, Math.max(lensRange.widestFNumber, n)) : n, referenceAperture = clamp(DEFAULTS.fNumber);
|
|
1587
|
+
// (N0/8)² is exactly 1 at f/8, so f/8 keeps 1/125 s exactly.
|
|
1588
|
+
const sameShutter = DEFAULTS.shutterSeconds * (referenceAperture / DEFAULTS.fNumber) ** 2;
|
|
1589
|
+
const reference = { aperture: referenceAperture, shutterSeconds: Math.max(sameShutter, capabilities.shutter.minimumSeconds), iso: DEFAULTS.iso };
|
|
1522
1590
|
const referenceLuminance = sosReferenceLuminance(reference.aperture, reference.shutterSeconds, reference.iso);
|
|
1523
|
-
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²)
|
|
1591
|
+
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²)" +
|
|
1592
|
+
(referenceAperture === DEFAULTS.fNumber ? "." : "; the lens excludes f/8: " + (reference.shutterSeconds > sameShutter ? "the shortest shutter" : "the same N²/t") + " at f/" + referenceAperture + "."));
|
|
1524
1593
|
const meterProfile = body.metering[0].profile, zones = 16, zoneSamples = [];
|
|
1525
1594
|
let luminanceSum = 0;
|
|
1526
1595
|
for (let j = 0; j < zones; j++)
|
|
@@ -1551,12 +1620,13 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1551
1620
|
target = setExposureCompensationOnMeterTarget({ targetId: "facade-target-ec", baseTarget: target, exposureCompensationStops: compensation });
|
|
1552
1621
|
let resolution;
|
|
1553
1622
|
let fNumber, shutterSeconds;
|
|
1623
|
+
const checkFNumber = () => {
|
|
1624
|
+
if (clamp(fNumber) !== fNumber)
|
|
1625
|
+
fail("lens.fNumber " + fNumber + " is outside the " + lensRange.name + " range f/" + lensRange.widestFNumber + "–f/" + lensRange.narrowestFNumber + ".");
|
|
1626
|
+
};
|
|
1554
1627
|
if (mode === "manual" || mode === "aperture-priority") {
|
|
1555
1628
|
fNumber = positive(pick(lensOptions, "fNumber", "lens.fNumber", DEFAULTS.fNumber, "f/8."), "lens.fNumber");
|
|
1556
|
-
|
|
1557
|
-
fail("lens.fNumber " + fNumber + " is outside the " + prime.lensId + " range f/" + prime.widestFNumber + "–f/" + prime.narrowestFNumber + ".");
|
|
1558
|
-
if (catalogue && (fNumber < catalogue.widestFNumber || fNumber > catalogue.narrowestFNumber))
|
|
1559
|
-
fail("lens.fNumber " + fNumber + " is outside the " + catalogue.name + " range f/" + catalogue.widestFNumber + "–f/" + catalogue.narrowestFNumber + ".");
|
|
1629
|
+
checkFNumber();
|
|
1560
1630
|
}
|
|
1561
1631
|
else {
|
|
1562
1632
|
if (lensOptions.fNumber !== undefined)
|
|
@@ -1653,8 +1723,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1653
1723
|
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1654
1724
|
}
|
|
1655
1725
|
else {
|
|
1656
|
-
|
|
1657
|
-
|
|
1726
|
+
// #583: a lens range without f/4–f/16 limits the line to it; every other request keeps the line and its assumption.
|
|
1727
|
+
const programLine = defaultProgramLine(clamp(4), clamp(16), reference);
|
|
1728
|
+
// A limited line (#583) records its nodes under the profile id "facade-generic-program-line-lens-range".
|
|
1729
|
+
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.");
|
|
1730
|
+
resolution = resolveProgramAutoExposureMode({ target, capabilities: resolverCapabilities, referenceExposure: reference, programLine,
|
|
1658
1731
|
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1659
1732
|
{ kind: "manual", iso } });
|
|
1660
1733
|
}
|
|
@@ -1671,11 +1744,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1671
1744
|
const exposureGreyCardDeviationStops = callerScene?.kind === "radiance-function" ? null : greyCardDeviationStops(fNumber, shutterSeconds, resolvedIso, illuminanceLux);
|
|
1672
1745
|
// #549: the catalogue lens's character at the resolved f-number and focus: focus scatter, the defocus disc folded into its PSF, then
|
|
1673
1746
|
// its distortion and vignetting profiles over the field the PSF and the queries reach, and its PSF profile.
|
|
1674
|
-
let catalogueTaps, catalogueDefocusApplied = false;
|
|
1747
|
+
let catalogueTaps, catalogueDefocusApplied = false, catalogueKernelRadius = 0;
|
|
1675
1748
|
if (catalogue && lensKit) {
|
|
1676
1749
|
const { GENERIC_LENS_EVIDENCE, genericLensDistortionProfile, genericLensVignettingProfile, resolveGenericLens, simulateGenericLensFocusError, createGenericLensPsfProfile, genericLensPsfKernelRadius, GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES } = lensKit;
|
|
1677
|
-
|
|
1678
|
-
fail("lens.fNumber " + fNumber + " is outside the " + catalogue.name + " range f/" + catalogue.widestFNumber + "–f/" + catalogue.narrowestFNumber + ".");
|
|
1750
|
+
checkFNumber();
|
|
1679
1751
|
if (lensOptions.focusScatter !== undefined) {
|
|
1680
1752
|
const scatterOptions = allowKeys(lensOptions.focusScatter, "lens.focusScatter", ["seed", "shotIndex"]);
|
|
1681
1753
|
const sample = simulateGenericLensFocusError({ lensId: catalogue.lensId, activeImagingArea: catalogueFrame, fNumber, focusDistanceM, seed: scatterOptions.seed,
|
|
@@ -1743,6 +1815,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1743
1815
|
(built.kernelRadiusSites - built.aberrationKernelRadiusSites) + "), over the limit of " + GENERIC_LENS_PSF_MAXIMUM_COMBINED_RADIUS_SITES + " (" + (2 * built.kernelRadiusSites + 1) ** 2 +
|
|
1744
1816
|
" scene queries per site and spectral node through the environment quadrature). Bring the focus closer to the subject, stop down, or use larger pixels.");
|
|
1745
1817
|
catalogueTaps = (2 * built.kernelRadiusSites + 1) ** 2;
|
|
1818
|
+
catalogueKernelRadius = built.kernelRadiusSites;
|
|
1746
1819
|
psfReport = { aberrationKernelRadiusSites: built.aberrationKernelRadiusSites, kernelRadiusSites: built.kernelRadiusSites, maximumTruncatedEnergyFraction: built.maximumTruncatedEnergyFraction,
|
|
1747
1820
|
fieldNodesMm: { x: built.fieldXmm, y: built.fieldYmm }, wavelengthNodesNm: built.wavelengthsNm, transformSize: built.transformSize, maximumPhaseStepWaves: built.maximumPhaseStepWaves };
|
|
1748
1821
|
}
|
|
@@ -2312,12 +2385,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2312
2385
|
// that only captures does not contain the display view.
|
|
2313
2386
|
// #563: the view's expected signal through the lens, from the capture's own geometry (#548), checked to cover its farthest source point: the outer
|
|
2314
2387
|
// site centre plus the kernel radius and the largest query offset.
|
|
2315
|
-
|
|
2388
|
+
// #569: a catalogue lens's field-dependent PSF, read per field zone by the lens signal (display-view-lens-signal.ts, loaded on demand).
|
|
2389
|
+
const viewLensKit = displayView && lensGeometry ? await import("./display-view-lens-signal.js") : undefined;
|
|
2390
|
+
const displayViewLensPsf = viewLensKit && catalogue && lensPsf ?
|
|
2391
|
+
viewLensKit.createDisplayViewLensPsf(lensPsf.configuration.psf, catalogueKernelRadius, nativeWidth, nativeHeight, pitch) : undefined;
|
|
2392
|
+
if (displayViewLensPsf)
|
|
2393
|
+
assume("view.lensPsf", "model", { method: "piecewise-constant-field-zones", zoneSites: displayViewLensPsf.zoneSites, kernelRadiusSites: catalogueKernelRadius, scientificStatus: "approximation" }, "Each site takes the lens PSF at its field zone's centre, where the capture resolves it at each query point (docs/DISPLAY_VIEW.md, \"Catalogue lenses\").");
|
|
2394
|
+
const lensSignal = viewLensKit ? viewLensKit.prepareDisplayViewLensSignal({ geometry: lensGeometry,
|
|
2316
2395
|
times: rotating ? Array.from({ length: temporalSampleCount }, (_, i) => (i + 0.5) * (shutterSeconds / temporalSampleCount)) : [0], distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
2317
|
-
pitch, imagingArea, boxSamples, spots: antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }] }, nativeWidth, nativeHeight, (diffraction ? diffraction.kernelRadiusSamples : 0) + (displayViewDefocus ? (bokehAxisKernel?.radius ?? defocusDiscKernelRadius(displayViewDefocus.diameterSites)) : 0) +
|
|
2318
|
-
Math.max(0, ...queryOffsetsSites.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))) - 0.5, imageSpaceOptions.join(", ")) : undefined;
|
|
2396
|
+
pitch, imagingArea, boxSamples, spots: antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }], psf: displayViewLensPsf, zeroBeyondHorizon: catalogue?.kind === "fisheye" }, nativeWidth, nativeHeight, (diffraction ? diffraction.kernelRadiusSamples : 0) + (displayViewDefocus ? (bokehAxisKernel?.radius ?? defocusDiscKernelRadius(displayViewDefocus.diameterSites)) : 0) + catalogueKernelRadius +
|
|
2397
|
+
Math.max(0, ...queryOffsetsSites.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y)))) - 0.5, catalogue ? "lens.catalogue" : imageSpaceOptions.join(", ")) : undefined;
|
|
2319
2398
|
if (displayView)
|
|
2320
|
-
return await renderView({ lens: lensSignal, 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 });
|
|
2399
|
+
return await renderView({ lens: lensSignal, lensPsf: displayViewLensPsf, ahd, fastReport, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, materialLayers, chartReport, chartSize, diffraction, defocus: displayViewDefocus, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, exposureGain, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, isoReport, standardExposureDeviationStops: exposureDeviationStops, standardExposureIlluminanceLux: exposureIlluminanceLux, greyCardDeviationStops: exposureGreyCardDeviationStops, metering, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl, kelvinWhiteBalanceInput });
|
|
2321
2400
|
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
2322
2401
|
if (idealReference) {
|
|
2323
2402
|
const referenceStarted = clockMs();
|
|
@@ -2435,18 +2514,43 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2435
2514
|
// pupil rays or caller PSF) takes its expected electrons from the tabulated signal, verified per CFA phase against the chain.
|
|
2436
2515
|
const tabulatedScene = chart !== undefined || callerScene?.kind === "material-function";
|
|
2437
2516
|
// #391: a DNG from region parts takes the whole frame's RAW from the parts instead of rendering it.
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2517
|
+
// #571: simulateCaptureFromParts takes the RAW from the parts in the same way, then develops it as a capture does.
|
|
2518
|
+
const dngParts = run === "dng-from-parts" || run === "develop-from-parts" ? viewOption : undefined, partsCaller = run === "dng-from-parts" ? "serializeSimulatedCaptureDngFromParts" : "simulateCaptureFromParts";
|
|
2519
|
+
if (dngParts !== undefined && regionNative)
|
|
2520
|
+
fail(partsCaller + " takes the whole frame: the options take no output.region.");
|
|
2441
2521
|
let tabulated;
|
|
2442
|
-
// #
|
|
2443
|
-
|
|
2522
|
+
// #568: lens.catalogue's fast method: tabulated expected electrons with the PSF blended over a field grid (catalogue-lens-signal.ts), for a
|
|
2523
|
+
// still camera, site-centre queries and a chart or material scene; verified per CFA phase against the exact chain within its tolerance.
|
|
2524
|
+
let catalogueFastRun;
|
|
2525
|
+
const catalogueFastEligible = catalogueFastRequested && catalogueTaps !== undefined && lensKit !== undefined && lensPsf !== undefined && dngParts === undefined && tabulatedScene &&
|
|
2526
|
+
!pupil && !rotating && boxSamples === 1 && !antiAliasing && distortion?.kind === "radial" && provider.chartMaterialAt !== undefined && provider.materialRadiance !== undefined;
|
|
2527
|
+
// The tabulated paths' channel response per spectral node and material tables (the fast catalogue path and the expected-signal path).
|
|
2528
|
+
const tabulatedTables = () => {
|
|
2444
2529
|
const { electronsPerRadiance, wavelengths } = axisChannelResponse("simulateCapture");
|
|
2445
|
-
const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0));
|
|
2446
2530
|
const sceneMaterials = chart ? chart.materials : callerScene.materials, materialRadiance = provider.materialRadiance;
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
...(
|
|
2531
|
+
return { wavelengths, perNode: CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0)),
|
|
2532
|
+
materials: { materialAt: provider.chartMaterialAt, whiteRadiance: (nm) => irradianceScale * illuminant.shape.evaluate(nm) / Math.PI,
|
|
2533
|
+
list: [...sceneMaterials.map((m, index) => (m.kind === "neutral" ? { neutral: m.reflectance } : { radiance: (nm) => materialRadiance(index, nm) })),
|
|
2534
|
+
...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])] } };
|
|
2535
|
+
};
|
|
2536
|
+
if (catalogueFastEligible) {
|
|
2537
|
+
const { wavelengths, perNode, materials } = tabulatedTables();
|
|
2538
|
+
const { signalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(1), boxSamples: 1, antiAliasing: undefined, pitch,
|
|
2539
|
+
materials, radianceAt: provider.radianceAt, imagingArea, focalLengthMm, focusDistanceM, subjectDistanceM, metresPerFrameUnit, caller: "simulateCapture" });
|
|
2540
|
+
const radial = distortion.profile;
|
|
2541
|
+
const signal = lensKit.createCatalogueLensSignal({ nativeWidth, nativeHeight, pitch, psf: lensPsf.configuration.psf, wavelengthsNm: wavelengths, electronsPerRadiance: perNode,
|
|
2542
|
+
scene: signalInput.scene, channelAt: signalInput.channelAt,
|
|
2543
|
+
distortion: { normalizationRadiusMm: radial.normalizationRadiusMm, coefficients: { k1: radial.coefficients.k1, k2: radial.coefficients.k2 ?? 0, k3: radial.coefficients.k3 ?? 0 } },
|
|
2544
|
+
...(vignetting ? { vignetting } : {}), projection: projection === "fisheye-equisolid-angle" ? projection : "rectilinear", outsideImageCircleSeesNothing: catalogue.kind === "fisheye",
|
|
2545
|
+
focalLengthMm, focusDistanceM, distanceM: subjectDistanceM, metresPerFrameUnit });
|
|
2546
|
+
tabulated = await renderTabulatedNativeRaw({ input: environmentRaw, region: regionNative, siteInput, evaluateRadiance: rawProvider().evaluateRadiance,
|
|
2547
|
+
electrons: signal, relativeTolerance: lensKit.CATALOGUE_FAST_VERIFICATION_TOLERANCE, onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
2548
|
+
if (tabulated)
|
|
2549
|
+
catalogueFastRun = { ...signal.grid, verificationRelativeDifference: tabulated.verificationRelativeDifference };
|
|
2550
|
+
}
|
|
2551
|
+
// #553: a cat's eye varies the kernel over the field, which the expected-signal tables (one kernel per node) do not take.
|
|
2552
|
+
else if (dngParts === undefined && tabulatedScene && !pupil && !lensFieldMapping && !vignetting && !lensPsf && !rotating && !bokehZones && (!diffraction || diffractionObjectSpace) && provider.chartMaterialAt && provider.materialRadiance) {
|
|
2553
|
+
const { wavelengths, perNode, materials: tabulatedMaterials } = tabulatedTables();
|
|
2450
2554
|
const { signalInput: builtSignalInput } = buildExpectedSignalInput({ nativeWidth, nativeHeight, width, wavelengths, perNode, apertureNodes: apertureNodeOffsets(boxSamples), boxSamples, antiAliasing, pitch,
|
|
2451
2555
|
diffraction: diffraction && !defocusSiteKernels ? { radius: diffraction.kernelRadiusSamples, psf: diffractionPsf.configuration.psf, profileWavelengths: diffraction.profile.axes.wavelengthNm } : undefined,
|
|
2452
2556
|
materials: tabulatedMaterials, radianceAt: provider.radianceAt, imagingArea, focalLengthMm, focusDistanceM, subjectDistanceM, metresPerFrameUnit, caller: "simulateCapture" });
|
|
@@ -2472,8 +2576,18 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2472
2576
|
electrons: createDisplayViewExpectedSignal(signalInput), onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
2473
2577
|
}
|
|
2474
2578
|
let rawOutput;
|
|
2475
|
-
if (dngParts !== undefined)
|
|
2476
|
-
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight), providerEvaluationCount: 0 };
|
|
2579
|
+
if (dngParts !== undefined) {
|
|
2580
|
+
rawOutput = { raw: joinNativeRawParts(dngParts, plan, nativeWidth, nativeHeight, partsCaller), providerEvaluationCount: 0 };
|
|
2581
|
+
if (run === "develop-from-parts")
|
|
2582
|
+
assume("parts", "model", { partCount: dngParts.length }, "The whole render's RAW, joined from region renders of this request (#571); how they were rendered is in their results.");
|
|
2583
|
+
}
|
|
2584
|
+
else if (tabulated && catalogueFastRun) {
|
|
2585
|
+
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2586
|
+
fastApplied["lens.catalogue"] = true;
|
|
2587
|
+
fastReport = Object.freeze({ ...fastReport, applied: Object.freeze({ ...fastApplied }), assurance: "approximation-with-a-priori-bound" });
|
|
2588
|
+
for (const a of lensKit.catalogueFastAssumptions(catalogueFastRun))
|
|
2589
|
+
assume(a.id, "model", a.value, a.explanation);
|
|
2590
|
+
}
|
|
2477
2591
|
else if (tabulated) {
|
|
2478
2592
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2479
2593
|
// The verification's relative difference itself is not reported: it is a last-bits quantity, and captures use the JavaScript
|
|
@@ -2563,7 +2677,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2563
2677
|
assume("output.dng", "model", { writer: "serializeNativeRawDng", schemaVersion: dng.schemaVersion, whiteLevel: dng.whiteLevel, blackLevels: dng.blackLevels,
|
|
2564
2678
|
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.");
|
|
2565
2679
|
}
|
|
2566
|
-
if (
|
|
2680
|
+
if (run === "dng-from-parts")
|
|
2567
2681
|
return dng;
|
|
2568
2682
|
// Development
|
|
2569
2683
|
const development = { phaseProfiles, colorProfile,
|
|
@@ -2596,60 +2710,94 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2596
2710
|
"Below f_c the gain is bounded, so attenuated detail is only partly restored and noise is amplified by up to the measured responsePeakGain. " +
|
|
2597
2711
|
"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).");
|
|
2598
2712
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
const
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2713
|
+
// #570: the lens corrections after the diffraction correction (which acts on the sensor's own lattice), before the tone curve.
|
|
2714
|
+
let cornerStops = null;
|
|
2715
|
+
if (lensCorrection) {
|
|
2716
|
+
const s = lensCorrectionSelection, c = lensCorrection, k = (channel) => c.scales[channel];
|
|
2717
|
+
if (s.vignetting)
|
|
2718
|
+
cornerStops = lensKit.resolveGenericLens({ lensId: catalogue.lensId, activeImagingArea: catalogueFrame, fNumber }).character.vignetting.cornerStops;
|
|
2719
|
+
assume("output.lensCorrections", "model", { profileId: "photivra-" + catalogue.lensId + ":corrections", ...s, k1: c.k1, channelScales: { red: k(0), green: k(1), blue: k(2) },
|
|
2720
|
+
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 " +
|
|
2721
|
+
"(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). " +
|
|
2722
|
+
"Corners lose resolution, corner noise rises with the gain, axial colour stays. The DNG is not corrected.");
|
|
2723
|
+
}
|
|
2724
|
+
// Row progress of a development stage, yielding to the host about every 8 ms. #571: the detail stages run row by row in place
|
|
2725
|
+
// (one developed image in memory) and report each output row.
|
|
2726
|
+
const rowProgress = (stage) => {
|
|
2727
|
+
let lastYield = Date.now();
|
|
2728
|
+
return async (completed, total) => {
|
|
2729
|
+
onProgress?.({ stage, completedTileCount: completed, totalTileCount: total });
|
|
2730
|
+
if (Date.now() - lastYield >= 8) {
|
|
2731
|
+
await yieldControl();
|
|
2732
|
+
checkAbort();
|
|
2733
|
+
lastYield = Date.now();
|
|
2734
|
+
}
|
|
2735
|
+
};
|
|
2736
|
+
};
|
|
2737
|
+
const detailRows = async (stage, rows) => {
|
|
2738
|
+
const tick = rowProgress(stage);
|
|
2739
|
+
let step = rows.next();
|
|
2740
|
+
for (; !step.done; step = rows.next())
|
|
2741
|
+
await tick(step.value, outputHeight);
|
|
2742
|
+
return step.value;
|
|
2743
|
+
};
|
|
2744
|
+
const correctLinear = async (rgb) => {
|
|
2745
|
+
if (diffractionFilter) {
|
|
2746
|
+
const corrected = await detailRows("diffraction-correction", correctDiffractionInPlace(rgb, outputWidth, outputHeight, diffractionFilter));
|
|
2747
|
+
detailDiagnostics.diffractionCorrection = { ...corrected, cutoffCyclesPerPixel: diffractionFilter.cutoffCyclesPerPixel,
|
|
2748
|
+
responsePeakGain: diffractionFilter.responsePeakGain, responseMaximumBeyondCutoff: diffractionFilter.responseMaximumBeyondCutoff };
|
|
2749
|
+
}
|
|
2750
|
+
if (lensCorrection) {
|
|
2751
|
+
const into = imageRegion ?? { x: 0, y: 0, width, height };
|
|
2752
|
+
rgb = lensKit.applyDevelopedLensCorrection(lensCorrection, rgb, region ?? into, into, cornerStops);
|
|
2753
|
+
outputWidth = into.width;
|
|
2754
|
+
outputHeight = into.height;
|
|
2755
|
+
}
|
|
2756
|
+
return rgb;
|
|
2606
2757
|
};
|
|
2607
2758
|
if (sharpening)
|
|
2608
2759
|
assume("output.sharpening", "model", { model: "development-detail", modelVersion: DEVELOPMENT_DETAIL_MODEL_VERSION, method: "unsharp-mask", ...sharpening,
|
|
2609
2760
|
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], " +
|
|
2610
2761
|
"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) " +
|
|
2611
2762
|
"and sharpens noise above the threshold. It adds no optical detail.");
|
|
2612
|
-
if (detailRequested)
|
|
2763
|
+
if (detailRequested || lensCorrection)
|
|
2613
2764
|
assume("output.developmentOrder", "model", [...(exposureGain === 1 ? [] : ["development exposure push (RAW)"]), "demosaic", "colour and white balance", ...(diffractionCorrectionOption ? ["diffraction correction (linear)"] : []),
|
|
2765
|
+
...(lensCorrection ? ["lens corrections (linear)"] : []),
|
|
2614
2766
|
"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." +
|
|
2615
2767
|
(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."));
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
const
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2768
|
+
// Wide gamut: linear sRGB/D65 → target primaries (perceptual gamut mapping when asked), encoded.
|
|
2769
|
+
const encodeLinear = (rgb, highlight) => {
|
|
2770
|
+
const mapping = perceptual ? prepareRgbGamutMapping({ colorSpace: colorEncoding, method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION }) : undefined;
|
|
2771
|
+
const encoded = encodeSdrFromLinear(rgb, colorEncoding, pngBitDepth, mapping);
|
|
2772
|
+
if (mapping)
|
|
2773
|
+
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2774
|
+
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2775
|
+
return { raw: rawOutput.raw, integerSamples: encoded.samples, highlight, diagnostics: encoded.diagnostics };
|
|
2776
|
+
};
|
|
2777
|
+
if (ahd || ((detailRequested || lensCorrection) && !linearPath)) {
|
|
2778
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"), ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined);
|
|
2779
|
+
const rgb = await correctLinear(linear.rgb);
|
|
2780
|
+
if (!linearPath) {
|
|
2781
|
+
let codes, counts;
|
|
2782
|
+
if (sharpening) {
|
|
2783
|
+
const rendered = await detailRows("sharpening", renderSdrWithUnsharpMaskInPlace(rgb, outputWidth, outputHeight, rendering, sharpening));
|
|
2784
|
+
detailDiagnostics.sharpening = rendered.sharpening;
|
|
2785
|
+
({ codes, counts } = rendered);
|
|
2786
|
+
}
|
|
2787
|
+
else {
|
|
2788
|
+
// The native development's SDR kernel, row by row, on the AHD-developed linear sRGB.
|
|
2789
|
+
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
2790
|
+
codes = rendering.bitDepth === 8 ? new Uint8Array(outputWidth * outputHeight * 3) : new Uint16Array(outputWidth * outputHeight * 3);
|
|
2791
|
+
counts = { negative: 0, above: 0, changed: 0, low: 0, high: 0, outOfGamut: false };
|
|
2792
|
+
for (let y = 0; y < outputHeight; y++)
|
|
2793
|
+
renderSdrRunInto(rgb, y * outputWidth * 3, outputWidth, rendering, tone, 2 ** rendering.renderingExposureEv, maximum, codes, y * outputWidth * 3, scratch, counts);
|
|
2624
2794
|
}
|
|
2625
|
-
}, ahd ? { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } } : undefined);
|
|
2626
|
-
const rgb = correctLinear(linear.rgb);
|
|
2627
|
-
if (!linearPath && sharpening) {
|
|
2628
|
-
const rendered = renderSdrWithUnsharpMask(rgb, outputWidth, outputHeight, rendering, sharpening);
|
|
2629
|
-
detailDiagnostics.sharpening = rendered.sharpening;
|
|
2630
|
-
developed = { raw: rawOutput.raw, integerSamples: rendered.codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: rendered.counts.negative,
|
|
2631
|
-
renderingAboveReferenceSampleCount: rendered.counts.above, toneChangedSampleCount: rendered.counts.changed, gamutClippedLowSampleCount: rendered.counts.low,
|
|
2632
|
-
gamutClippedHighSampleCount: rendered.counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
2633
|
-
}
|
|
2634
|
-
else if (!linearPath) {
|
|
2635
|
-
// The native development's SDR kernel, row by row, on the AHD-developed linear sRGB.
|
|
2636
|
-
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
2637
|
-
const codes = rendering.bitDepth === 8 ? new Uint8Array(outputWidth * outputHeight * 3) : new Uint16Array(outputWidth * outputHeight * 3);
|
|
2638
|
-
const counts = { negative: 0, above: 0, changed: 0, low: 0, high: 0, outOfGamut: false };
|
|
2639
|
-
for (let y = 0; y < outputHeight; y++)
|
|
2640
|
-
renderSdrRunInto(rgb, y * outputWidth * 3, outputWidth, rendering, tone, 2 ** rendering.renderingExposureEv, maximum, codes, y * outputWidth * 3, scratch, counts);
|
|
2641
2795
|
developed = { raw: rawOutput.raw, integerSamples: codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: counts.negative,
|
|
2642
2796
|
renderingAboveReferenceSampleCount: counts.above, toneChangedSampleCount: counts.changed, gamutClippedLowSampleCount: counts.low,
|
|
2643
2797
|
gamutClippedHighSampleCount: counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
2644
2798
|
}
|
|
2645
|
-
else
|
|
2646
|
-
|
|
2647
|
-
const encoded = encodeSdrFromLinear(rgb, colorEncoding, pngBitDepth, mapping);
|
|
2648
|
-
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
2649
|
-
if (mapping)
|
|
2650
|
-
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2651
|
-
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2652
|
-
}
|
|
2799
|
+
else
|
|
2800
|
+
developed = encodeLinear(rgb, linear.highlight);
|
|
2653
2801
|
}
|
|
2654
2802
|
else if (!linearPath) {
|
|
2655
2803
|
const devTask = createNativeRawDevelopmentTask({ ...development, raw: rawOutput.raw }, { yieldControl,
|
|
@@ -2659,22 +2807,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2659
2807
|
}
|
|
2660
2808
|
else {
|
|
2661
2809
|
// Wide gamut: the native development operator up to linear sRGB/D65 (unclipped), then XYZ → target primaries.
|
|
2662
|
-
|
|
2663
|
-
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, async (completed, total) => {
|
|
2664
|
-
onProgress?.({ stage: "development", completedTileCount: completed, totalTileCount: total });
|
|
2665
|
-
if (Date.now() - lastYield >= 8) {
|
|
2666
|
-
await yieldControl();
|
|
2667
|
-
checkAbort();
|
|
2668
|
-
lastYield = Date.now();
|
|
2669
|
-
}
|
|
2670
|
-
});
|
|
2810
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"));
|
|
2671
2811
|
{
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
2675
|
-
if (mapping) {
|
|
2676
|
-
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
2677
|
-
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
2812
|
+
developed = encodeLinear(await correctLinear(linear.rgb), linear.highlight);
|
|
2813
|
+
if (perceptual) {
|
|
2678
2814
|
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.");
|
|
2679
2815
|
}
|
|
2680
2816
|
assume("output.colorEncoding.conversion", "model", { from: "linear-srgb-d65 (unclipped)", via: "CIE XYZ", to: colorEncoding, gamutHandling: perceptual ? "cielab-hue-preserving-chroma-compression" : "clip-components",
|
|
@@ -2733,14 +2869,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2733
2869
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
2734
2870
|
...(highlightMode === "reconstruct" ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
2735
2871
|
...(perceptual ? ["output.gamutMapping.perceptual"] : []), ...(diffractionFilter ? ["output.diffractionCorrection"] : []),
|
|
2736
|
-
...(sharpening ? ["output.sharpening"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
2872
|
+
...(sharpening ? ["output.sharpening"] : []), ...(lensCorrection ? ["output.lensCorrections"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
2737
2873
|
// #552: the detail stages' contracts, in execution order inside the development.
|
|
2738
|
-
const detailContracts = diffractionFilter ? ["designDiffractionCorrectionFilter", "applyDiffractionCorrection"] : [], sdrContract = sharpening ? "renderSdrWithUnsharpMask" : "SDR rendering kernel";
|
|
2874
|
+
const detailContracts = [...(diffractionFilter ? ["designDiffractionCorrectionFilter", "applyDiffractionCorrection"] : []), ...(lensCorrection ? ["createGenericLensCorrectionProfile (developed linear colour)"] : [])], sdrContract = sharpening ? "renderSdrWithUnsharpMask" : "SDR rendering kernel";
|
|
2739
2875
|
return Object.freeze({
|
|
2740
2876
|
facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
2741
2877
|
image: { width: outputWidth, height: outputHeight, format, samples: developed.integerSamples, ...(jpeg ? { jpeg } : {}), ...(png ? { png, colorEncoding: colorEncoding } : {}), ...(dng ? { dng: dng.bytes } : {}) },
|
|
2742
2878
|
raw: developed.raw, plan: developmentPlan,
|
|
2743
|
-
...(region && regionNative ? { region: { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2879
|
+
...(region && regionNative ? { region: { ...imageRegion ?? region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2744
2880
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2745
2881
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" :
|
|
2746
2882
|
defocusImageSpace ? "image-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
@@ -2767,7 +2903,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2767
2903
|
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
2768
2904
|
...(ahd ? ["native RAW development operator (colour, after AHD)", ...detailContracts, ...(linearPath ? ["resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : [sdrContract])] :
|
|
2769
2905
|
linearPath ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, "resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] :
|
|
2770
|
-
detailRequested ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2906
|
+
detailRequested || lensCorrection ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2771
2907
|
...(jpeg ? ["createPrintJpegTask"] : []), ...(png ? ["serializePngImage"] : []), ...(dng ? ["resolveSensorReadoutRegime", "serializeNativeRawDng"] : [])],
|
|
2772
2908
|
limitations: ["Educational approximation: synthetic tier sensor values, idealized colorimetric sensor, planar Lambertian scene and point optics; not a calibrated camera emulation.",
|
|
2773
2909
|
"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."] },
|