@photivra/engine 2.18.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 +68 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- 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/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/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/generic-lens-capture.d.ts +4 -2
- package/dist/facade/generic-lens-capture.js +4 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +37 -5
- package/dist/facade/simulate-capture.js +153 -96
- package/dist/facade/simulate-capture.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 +4 -1
- package/dist/index.js +3 -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 +1 -0
- package/docs/API_REFERENCE.md +41 -2
- package/docs/QUICK_START.md +90 -8
- package/docs/USAGE.md +47 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +53 -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 +36 -5
- 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 +1 -0
- package/package.json +1 -1
|
@@ -90,7 +90,7 @@ import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
|
90
90
|
import { multiplyMatrices } from "../color/colorimetry-math.js";
|
|
91
91
|
import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
92
92
|
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
93
|
-
import {
|
|
93
|
+
import { correctDiffractionInPlace, defaultDiffractionCorrectionRadiusPixels, designDiffractionCorrectionFilter, parseDiffractionCorrectionSettings, parseUnsharpMaskSettings, renderSdrWithUnsharpMaskInPlace, DEVELOPMENT_DETAIL_MODEL_VERSION } from "../processing/development-detail.js";
|
|
94
94
|
import { parseSensorReadoutConversionProfile, resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
95
95
|
import { resolveChartScene, chartMaterialInputs, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
96
96
|
import { resolveChartMaterialLayers } from "./chart-material-layers.js";
|
|
@@ -107,7 +107,7 @@ export const SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES = 4;
|
|
|
107
107
|
export const SIMULATE_CAPTURE_COST_OPTIONS = /* @__PURE__ */ Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
108
108
|
"lens.diffraction", "lens.defocus", "lens.catalogue", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
109
109
|
"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"]);
|
|
110
|
+
"output.diffractionCorrection", "output.sharpening", "output.lensCorrections", "output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
111
111
|
/**
|
|
112
112
|
* The options that take the exact-or-fast flag (#395), in the path style of {@link SIMULATE_CAPTURE_COST_OPTIONS}: each accepts
|
|
113
113
|
* the record form with `fast` and is reported in `diagnostics.fast.applied` when it runs. Every one of them takes the flag,
|
|
@@ -553,20 +553,20 @@ function resolveAutomaticSlowestShutterOption(value, context) {
|
|
|
553
553
|
"reciprocal rule is a rule of thumb and is not used.");
|
|
554
554
|
return report;
|
|
555
555
|
}
|
|
556
|
-
function defaultProgramLine() {
|
|
556
|
+
function defaultProgramLine(wide, narrow, anchor) {
|
|
557
557
|
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
558
558
|
// 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
|
-
|
|
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",
|
|
561
564
|
scientificStatus: "approximation", policyKind: "generic-program-line", interpolation: "log2-aperture-shutter", evidence: FACADE_EVIDENCE,
|
|
562
565
|
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
|
-
] });
|
|
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 })) });
|
|
570
570
|
}
|
|
571
571
|
/** PNG colour signalling: the sRGB chunk for sRGB, ITU-T H.273 code points through cICP otherwise. */
|
|
572
572
|
function pngColourEncoding(encoding) {
|
|
@@ -744,13 +744,13 @@ export async function serializeSimulatedCaptureDngFromParts(options, parts) {
|
|
|
744
744
|
return await runCapture(options, "dng-from-parts", parts);
|
|
745
745
|
}
|
|
746
746
|
/** The whole frame's packed RAW from region parts (see `serializeSimulatedCaptureDngFromParts`). */
|
|
747
|
-
function joinNativeRawParts(value, plan, width, height) {
|
|
747
|
+
function joinNativeRawParts(value, plan, width, height, caller) {
|
|
748
748
|
if (!Array.isArray(value) || value.length === 0)
|
|
749
|
-
throw new InvalidConfigurationError("
|
|
749
|
+
throw new InvalidConfigurationError(caller + " needs at least one part.");
|
|
750
750
|
const parts = value, count = width * height;
|
|
751
751
|
const codes = new Uint16Array(count), blackLevels = new Uint16Array(count), digitalSaturationCodes = new Uint16Array(count), saturationFlags = new Uint8Array(count);
|
|
752
752
|
const covered = new Uint8Array(count), white = new Uint16Array(4);
|
|
753
|
-
const bad = (k, what) => { throw new InvalidConfigurationError("
|
|
753
|
+
const bad = (k, what) => { throw new InvalidConfigurationError("Part " + k + ": " + what); };
|
|
754
754
|
parts.forEach((part, k) => {
|
|
755
755
|
const rect = part?.region?.nativeRect, raw = part?.raw;
|
|
756
756
|
if (!rect || !raw)
|
|
@@ -792,9 +792,9 @@ function joinNativeRawParts(value, plan, width, height) {
|
|
|
792
792
|
}
|
|
793
793
|
});
|
|
794
794
|
if (covered.includes(0))
|
|
795
|
-
throw new InvalidConfigurationError("The
|
|
795
|
+
throw new InvalidConfigurationError("The parts do not cover every native site of the frame.");
|
|
796
796
|
if (white.includes(0))
|
|
797
|
-
throw new InvalidConfigurationError("The
|
|
797
|
+
throw new InvalidConfigurationError("The parts lack a white level for a CFA phase.");
|
|
798
798
|
return { plan, codes, blackLevels, digitalSaturationCodes, saturationFlags, effectiveSaturationCodes: white };
|
|
799
799
|
}
|
|
800
800
|
/**
|
|
@@ -1027,8 +1027,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1027
1027
|
}
|
|
1028
1028
|
else if (lensOptions.focusScatter !== undefined)
|
|
1029
1029
|
fail("lens.focusScatter needs lens.catalogue: the focus scatter is a catalogue lens's own.");
|
|
1030
|
-
const
|
|
1031
|
-
|
|
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;
|
|
1032
1032
|
const lensExposure = lensRange ? { ...lens.exposure, profileId: "photivra-" + lensRange.id + ":exposure",
|
|
1033
1033
|
focalLengthMmRange: { ...lens.exposure.focalLengthMmRange, value: { minimum: lensRange.focalLengthMm, maximum: lensRange.focalLengthMm } },
|
|
1034
1034
|
aperture: { ...lens.exposure.aperture, widestAvailableFNumber: { kind: "constant", fNumber: { value: lensRange.widestFNumber, evidence: FACADE_EVIDENCE } },
|
|
@@ -1181,7 +1181,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1181
1181
|
const diffractionObjectSpace = diffractionRadiusOption !== undefined && !rotating && !lensFieldMapping && !vignetting;
|
|
1182
1182
|
// Output and sensor
|
|
1183
1183
|
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"]);
|
|
1184
|
+
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng", "maximumPixels", "region", "metadata", "diffractionCorrection", "sharpening", "lensCorrections"]);
|
|
1185
1185
|
// Opt-in large images: a caller-raised native pixel limit. Without it the 4094-pixel sides are unchanged.
|
|
1186
1186
|
const raisedPixels = outputOptions.maximumPixels === undefined ? undefined : integerIn(outputOptions.maximumPixels, "output.maximumPixels", 1, NATIVE_RAW_LIMITS.ceilingPixels);
|
|
1187
1187
|
const maximumSide = raisedPixels === undefined ? 4094 : NATIVE_RAW_LIMITS.maximumDimension;
|
|
@@ -1303,6 +1303,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1303
1303
|
unsupported("output.highlightMode \"reconstruct\"", "the view supports the per-channel highlight clip.");
|
|
1304
1304
|
if (outputOptions.diffractionCorrection !== undefined || outputOptions.sharpening !== undefined)
|
|
1305
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.");
|
|
1306
1308
|
if (defocusBokeh?.catsEye)
|
|
1307
1309
|
unsupported("lens.defocus.catsEye", "the expected-signal tables hold one kernel per spectral node, and the cat's eye varies over the field; " +
|
|
1308
1310
|
"simulateCapture renders it. lens.defocus.blades and lens.defocus.sphericalAberration are supported.");
|
|
@@ -1355,23 +1357,46 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1355
1357
|
fail("output.demosaic \"ahd\" with mirror borders needs an output of at least 3 × 3 pixels.");
|
|
1356
1358
|
// Geometry: the demosaic border (if any) on each side; output crop is the active area.
|
|
1357
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
|
+
}
|
|
1358
1378
|
// Region (2.3): a rectangle of the whole frame. Its native sites are the region plus the demosaic reach on each side
|
|
1359
1379
|
// (clipped to the raster, where mirror borders apply exactly as in the whole render), with the origin rounded down to
|
|
1360
1380
|
// the 2 × 2 CFA repeat so the region raster keeps the frame's CFA phase.
|
|
1361
|
-
let region, regionNative;
|
|
1381
|
+
let region, regionNative, imageRegion;
|
|
1362
1382
|
if (outputOptions.region !== undefined) {
|
|
1363
1383
|
const r = allowKeys(outputOptions.region, "output.region", ["x", "y", "width", "height"]);
|
|
1364
1384
|
const x = integerIn(r.x, "output.region.x", 0, width - 1), y = integerIn(r.y, "output.region.y", 0, height - 1);
|
|
1365
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
|
+
}
|
|
1366
1391
|
const reach = margin > 0 ? margin : ahd ? AHD_MARGIN : builtInDemosaic ? builtInDemosaic.kernelReachSites : MARGIN;
|
|
1367
1392
|
let x0 = Math.max(0, region.x + margin - reach), y0 = Math.max(0, region.y + margin - reach);
|
|
1368
1393
|
x0 -= x0 % 2;
|
|
1369
1394
|
y0 -= y0 % 2;
|
|
1370
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 };
|
|
1371
1396
|
}
|
|
1372
|
-
|
|
1397
|
+
let outputWidth = region ? region.width : width, outputHeight = region ? region.height : height;
|
|
1373
1398
|
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), " +
|
|
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), " +
|
|
1375
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. " +
|
|
1376
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
|
|
1377
1402
|
// region results without diffraction stay byte-identical to 2.5.0.
|
|
@@ -1555,13 +1580,16 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1555
1580
|
assume("exposure.compensationStops", "default", 0, "No exposure compensation.");
|
|
1556
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
|
|
1557
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
|
|
1558
|
-
// luminance; every other request keeps f/8, 1/125 s.
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
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 };
|
|
1562
1590
|
const referenceLuminance = sosReferenceLuminance(reference.aperture, reference.shutterSeconds, reference.iso);
|
|
1563
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²)" +
|
|
1564
|
-
(referenceAperture === DEFAULTS.fNumber ? "." : "; the lens excludes f/8: the same N²/t at f/" + referenceAperture + "."));
|
|
1592
|
+
(referenceAperture === DEFAULTS.fNumber ? "." : "; the lens excludes f/8: " + (reference.shutterSeconds > sameShutter ? "the shortest shutter" : "the same N²/t") + " at f/" + referenceAperture + "."));
|
|
1565
1593
|
const meterProfile = body.metering[0].profile, zones = 16, zoneSamples = [];
|
|
1566
1594
|
let luminanceSum = 0;
|
|
1567
1595
|
for (let j = 0; j < zones; j++)
|
|
@@ -1593,7 +1621,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1593
1621
|
let resolution;
|
|
1594
1622
|
let fNumber, shutterSeconds;
|
|
1595
1623
|
const checkFNumber = () => {
|
|
1596
|
-
if (
|
|
1624
|
+
if (clamp(fNumber) !== fNumber)
|
|
1597
1625
|
fail("lens.fNumber " + fNumber + " is outside the " + lensRange.name + " range f/" + lensRange.widestFNumber + "–f/" + lensRange.narrowestFNumber + ".");
|
|
1598
1626
|
};
|
|
1599
1627
|
if (mode === "manual" || mode === "aperture-priority") {
|
|
@@ -1695,8 +1723,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1695
1723
|
{ kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
1696
1724
|
}
|
|
1697
1725
|
else {
|
|
1698
|
-
|
|
1699
|
-
|
|
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,
|
|
1700
1731
|
isoControl: automaticIso ? { kind: "automatic", ...autoIsoBaseline, ...limitsPolicy, ...(autoIsoShutterPolicy ? { slowestPreferredShutter: { ...autoIsoShutterPolicy } } : {}) } :
|
|
1701
1732
|
{ kind: "manual", iso } });
|
|
1702
1733
|
}
|
|
@@ -2483,9 +2514,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2483
2514
|
// pupil rays or caller PSF) takes its expected electrons from the tabulated signal, verified per CFA phase against the chain.
|
|
2484
2515
|
const tabulatedScene = chart !== undefined || callerScene?.kind === "material-function";
|
|
2485
2516
|
// #391: a DNG from region parts takes the whole frame's RAW from the parts instead of rendering it.
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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.");
|
|
2489
2521
|
let tabulated;
|
|
2490
2522
|
// #568: lens.catalogue's fast method: tabulated expected electrons with the PSF blended over a field grid (catalogue-lens-signal.ts), for a
|
|
2491
2523
|
// still camera, site-centre queries and a chart or material scene; verified per CFA phase against the exact chain within its tolerance.
|
|
@@ -2544,8 +2576,11 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2544
2576
|
electrons: createDisplayViewExpectedSignal(signalInput), onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
2545
2577
|
}
|
|
2546
2578
|
let rawOutput;
|
|
2547
|
-
if (dngParts !== undefined)
|
|
2548
|
-
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
|
+
}
|
|
2549
2584
|
else if (tabulated && catalogueFastRun) {
|
|
2550
2585
|
rawOutput = { raw: tabulated.raw, providerEvaluationCount: tabulated.providerEvaluationCount };
|
|
2551
2586
|
fastApplied["lens.catalogue"] = true;
|
|
@@ -2642,7 +2677,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2642
2677
|
assume("output.dng", "model", { writer: "serializeNativeRawDng", schemaVersion: dng.schemaVersion, whiteLevel: dng.whiteLevel, blackLevels: dng.blackLevels,
|
|
2643
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.");
|
|
2644
2679
|
}
|
|
2645
|
-
if (
|
|
2680
|
+
if (run === "dng-from-parts")
|
|
2646
2681
|
return dng;
|
|
2647
2682
|
// Development
|
|
2648
2683
|
const development = { phaseProfiles, colorProfile,
|
|
@@ -2675,60 +2710,94 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2675
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. " +
|
|
2676
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).");
|
|
2677
2712
|
}
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
const
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
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;
|
|
2685
2757
|
};
|
|
2686
2758
|
if (sharpening)
|
|
2687
2759
|
assume("output.sharpening", "model", { model: "development-detail", modelVersion: DEVELOPMENT_DETAIL_MODEL_VERSION, method: "unsharp-mask", ...sharpening,
|
|
2688
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], " +
|
|
2689
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) " +
|
|
2690
2762
|
"and sharpens noise above the threshold. It adds no optical detail.");
|
|
2691
|
-
if (detailRequested)
|
|
2763
|
+
if (detailRequested || lensCorrection)
|
|
2692
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)"] : []),
|
|
2693
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." +
|
|
2694
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."));
|
|
2695
|
-
|
|
2696
|
-
|
|
2697
|
-
const
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
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);
|
|
2703
2794
|
}
|
|
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
2795
|
developed = { raw: rawOutput.raw, integerSamples: codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: counts.negative,
|
|
2721
2796
|
renderingAboveReferenceSampleCount: counts.above, toneChangedSampleCount: counts.changed, gamutClippedLowSampleCount: counts.low,
|
|
2722
2797
|
gamutClippedHighSampleCount: counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
2723
2798
|
}
|
|
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
|
-
}
|
|
2799
|
+
else
|
|
2800
|
+
developed = encodeLinear(rgb, linear.highlight);
|
|
2732
2801
|
}
|
|
2733
2802
|
else if (!linearPath) {
|
|
2734
2803
|
const devTask = createNativeRawDevelopmentTask({ ...development, raw: rawOutput.raw }, { yieldControl,
|
|
@@ -2738,22 +2807,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2738
2807
|
}
|
|
2739
2808
|
else {
|
|
2740
2809
|
// 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
|
-
});
|
|
2810
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, rowProgress("development"));
|
|
2750
2811
|
{
|
|
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 };
|
|
2812
|
+
developed = encodeLinear(await correctLinear(linear.rgb), linear.highlight);
|
|
2813
|
+
if (perceptual) {
|
|
2757
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.");
|
|
2758
2815
|
}
|
|
2759
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",
|
|
@@ -2812,14 +2869,14 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2812
2869
|
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
2813
2870
|
...(highlightMode === "reconstruct" ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
2814
2871
|
...(perceptual ? ["output.gamutMapping.perceptual"] : []), ...(diffractionFilter ? ["output.diffractionCorrection"] : []),
|
|
2815
|
-
...(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] : [])]);
|
|
2816
2873
|
// #552: the detail stages' contracts, in execution order inside the development.
|
|
2817
|
-
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";
|
|
2818
2875
|
return Object.freeze({
|
|
2819
2876
|
facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
2820
2877
|
image: { width: outputWidth, height: outputHeight, format, samples: developed.integerSamples, ...(jpeg ? { jpeg } : {}), ...(png ? { png, colorEncoding: colorEncoding } : {}), ...(dng ? { dng: dng.bytes } : {}) },
|
|
2821
2878
|
raw: developed.raw, plan: developmentPlan,
|
|
2822
|
-
...(region && regionNative ? { region: { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2879
|
+
...(region && regionNative ? { region: { ...imageRegion ?? region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } } } : {}),
|
|
2823
2880
|
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
2824
2881
|
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" :
|
|
2825
2882
|
defocusImageSpace ? "image-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
@@ -2846,7 +2903,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
2846
2903
|
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
2847
2904
|
...(ahd ? ["native RAW development operator (colour, after AHD)", ...detailContracts, ...(linearPath ? ["resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : [sdrContract])] :
|
|
2848
2905
|
linearPath ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, "resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] :
|
|
2849
|
-
detailRequested ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2906
|
+
detailRequested || lensCorrection ? ["native RAW development operator (linear, pre-clip)", ...detailContracts, sdrContract] : ["createNativeRawDevelopmentTask"]),
|
|
2850
2907
|
...(jpeg ? ["createPrintJpegTask"] : []), ...(png ? ["serializePngImage"] : []), ...(dng ? ["resolveSensorReadoutRegime", "serializeNativeRawDng"] : [])],
|
|
2851
2908
|
limitations: ["Educational approximation: synthetic tier sensor values, idealized colorimetric sensor, planar Lambertian scene and point optics; not a calibrated camera emulation.",
|
|
2852
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."] },
|