@photivra/engine 2.19.0 → 2.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -1
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +7 -5
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -10
- package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
- package/dist/capture/dng-lens-correction-opcodes.js +119 -0
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
- package/dist/capture/native-environment-lean.js +1 -5
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +6 -0
- package/dist/capture/native-raw-dng.js +16 -2
- package/dist/capture/native-raw-dng.js.map +1 -1
- package/dist/capture/sensor-raw-producer.js +3 -5
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +2 -0
- package/dist/core/shared-helpers.js +4 -0
- package/dist/core/shared-helpers.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/generic-lens-catalogue.d.ts +8 -2
- package/dist/equipment/generic-lens-catalogue.js +31 -2
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.js +2 -3
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.js +2 -3
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +7 -4
- package/dist/facade/display-view-gpu.js +4 -4
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +1 -1
- package/dist/facade/display-view-lens-signal.js +5 -3
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.d.ts +3 -1
- package/dist/facade/display-view.js +1 -1
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/fast-methods.d.ts +50 -0
- package/dist/facade/fast-methods.js +50 -0
- package/dist/facade/fast-methods.js.map +1 -0
- package/dist/facade/fast-resolution.d.ts +12 -0
- package/dist/facade/fast-resolution.js +18 -0
- package/dist/facade/fast-resolution.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +3 -2
- package/dist/facade/generic-lens-capture.js +3 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +51 -11
- package/dist/facade/simulate-capture.js +144 -75
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +3 -2
- package/dist/facade/simulate-display-view.js +22 -12
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/aperture-diameter.d.ts +47 -0
- package/dist/optics/aperture-diameter.js +50 -0
- package/dist/optics/aperture-diameter.js.map +1 -0
- package/dist/optics/detail-limit.d.ts +182 -0
- package/dist/optics/detail-limit.js +336 -0
- package/dist/optics/detail-limit.js.map +1 -0
- package/dist/optics/generic-lens-psf.d.ts +18 -2
- package/dist/optics/generic-lens-psf.js +25 -2
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/crop-to-match-focal-length.d.ts +62 -0
- package/dist/output/crop-to-match-focal-length.js +63 -0
- package/dist/output/crop-to-match-focal-length.js.map +1 -0
- package/dist/output/developed-lens-corrections.js +2 -2
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +3 -1
- package/dist/output/generic-lens-corrections.js +8 -4
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +12 -2
- package/dist/output/lens-corrections.js +22 -7
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/print-contrast-reference.js +1 -1
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-size-blur-noise.d.ts +110 -0
- package/dist/output/print-size-blur-noise.js +138 -0
- package/dist/output/print-size-blur-noise.js.map +1 -0
- package/dist/sensor/eqe-electron-rate.js +1 -7
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +2 -0
- package/dist/sensor/exposure-window.js +2 -2
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/readout-timing.js +2 -23
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.js +1 -6
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +1 -7
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sensor-characteristics.d.ts +137 -0
- package/dist/sensor/sensor-characteristics.js +115 -0
- package/dist/sensor/sensor-characteristics.js.map +1 -0
- package/dist/sensor/spatial-sample-reduction.js +1 -5
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +1 -6
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +3 -0
- package/dist/sensor/temporal-photo-signal.js +5 -2
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/stabilization/system.d.ts +9 -0
- package/dist/stabilization/system.js +5 -6
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +76 -2
- package/docs/QUICK_START.md +88 -6
- package/docs/USAGE.md +65 -2
- package/docs/api/capture-native-raw-dng.md +6 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +7 -2
- package/docs/api/exports.json +152 -2
- package/docs/api/facade-display-view-gpu.md +5 -2
- package/docs/api/facade-fast-methods.md +91 -0
- package/docs/api/facade-simulate-capture.md +36 -11
- package/docs/api/facade-simulate-display-view.md +3 -2
- package/docs/api/optics-aperture-diameter.md +51 -0
- package/docs/api/optics-detail-limit.md +203 -0
- package/docs/api/output-crop-to-match-focal-length.md +67 -0
- package/docs/api/output-generic-lens-corrections.md +3 -1
- package/docs/api/output-lens-corrections.md +11 -1
- package/docs/api/output-print-size-blur-noise.md +115 -0
- package/docs/api/sensor-sensor-characteristics.md +105 -0
- package/llms.txt +4 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generic-lens-corrections.js","sourceRoot":"","sources":["../../src/output/generic-lens-corrections.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;GAMG;AAEH,OAAO,EAAE,2BAA2B,EAAE,MAAM,uBAAuB,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AACpF,OAAO,EAAE,wBAAwB,EAA4B,MAAM,+BAA+B,CAAC;AACnG,OAAO,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,MAAM,wCAAwC,CAAC;AAC1H,OAAO,EAAE,iCAAiC,EAAiE,MAAM,uBAAuB,CAAC;AAEzI,OAAO,EAAE,uBAAuB,EAAE,MAAM,iCAAiC,CAAC;AAE1E,OAAO,EAAE,8CAA8C,EAAE,MAAM,iCAAiC,CAAC;AAWjG
|
|
1
|
+
{"version":3,"file":"generic-lens-corrections.js","sourceRoot":"","sources":["../../src/output/generic-lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The generic correction profile of a catalogue lens (#549), for the post-capture lens corrections\n * (`resolveLensCorrectionPlan`, `calculateLensCorrectedCapture`): a distortion map, a lateral-colour map per channel and a\n * peripheral-illumination gain, each from the lens's own character (`resolveGenericLens`). Consumer lenses rely on it more:\n * their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is\n * a labelled approximation, bound to one exact camera state (`OpticalProfileState`).\n */\n\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { parseOpticalProfileState, type OpticalProfileState } from \"../optics/profile-contract.js\";\nimport { GENERIC_LENS_EVIDENCE, genericLensDispersion, resolveGenericLens } from \"../equipment/generic-lens-catalogue.js\";\nimport { parseGenericLensCorrectionProfile, type GenericLensCorrectionProfile, type LensCorrectionChannel } from \"./lens-corrections.js\";\nimport type { AffineGeometricTransform } from \"./geometric-transforms.js\";\nimport { genericLensChannelScale } from \"./developed-lens-corrections.js\";\n\nexport { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM } from \"./developed-lens-corrections.js\";\n\nexport interface CreateGenericLensCorrectionProfileInput {\n /** A catalogue id (`GENERIC_LENS_IDS`); `state.lensId` must be the same id. */\n lensId: string;\n /** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */\n activeImagingArea: { widthMm: number; heightMm: number };\n /** The exact camera state the profile applies to: its focal length must be the lens's on this sensor, its aperture in range. */\n state: OpticalProfileState;\n}\n\n/**\n * Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own\n * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the\n * relative-illumination gain at the state's aperture (strength 1). From profile version 1.1.0 (#584) the vignetting component\n * (version 1.1.0) carries the distortion's area factor (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's\n * sensor point, so it flattens a flat-field capture. The result passes `parseGenericLensCorrectionProfile`.\n */\nexport function createGenericLensCorrectionProfile(input: CreateGenericLensCorrectionProfileInput): GenericLensCorrectionProfile {\n requireInputObject(input, \"createGenericLensCorrectionProfile input\");\n requireInputKeys(input, \"createGenericLensCorrectionProfile\", [\"lensId\", \"activeImagingArea\", \"state\"]);\n const state = parseOpticalProfileState(input.state);\n const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea, fNumber: state.aperture });\n if (state.lensId !== lens.lensId) throw new InvalidScientificInputError(\"state.lensId must be the catalogue id \" + lens.lensId + \".\");\n if (Math.abs(state.focalLengthMm - lens.focalLengthMm) > 1e-9 * lens.focalLengthMm)\n throw new InvalidScientificInputError(\"state.focalLengthMm must be the \" + lens.name + \"'s focal length on this sensor, \" + lens.focalLengthMm + \" mm.\");\n const c = lens.character, domain = \"reconstructed-linear\" as const;\n const identity = { version: \"1.0.0\", domain, availability: \"toggle\" as const, defaultEnabled: true, dependencies: [] as string[], requiredByStabilizationModes: [] as string[] };\n const scale = (channel: LensCorrectionChannel): number => genericLensChannelScale(c.lateralColor.fraction, genericLensDispersion, channel);\n const affine = (channel: LensCorrectionChannel): AffineGeometricTransform => ({ id: lens.lensId + \":lateral-ca:\" + channel, version: \"1.0.0\", domain, purpose: \"lateral-ca\" as const, kind: \"affine\" as const,\n matrix: [scale(channel), 0, 0, scale(channel)] as [number, number, number, number], offsetMm: { x: 0, y: 0 } });\n return parseGenericLensCorrectionProfile({ schemaVersion: \"0.1.0\", id: \"photivra-\" + lens.lensId + \":corrections\", version: \"1.1.0\", state,\n evidence: { kind: \"generic-parametric\", basis: \"Generic catalogue lens \" + lens.name + \" (#549): the inverse of its own distortion, lateral colour and vignetting.\",\n residualNote: \"Generic approximation, not a measured lens; axial colour, coma and the other aberrations stay.\", sources: [...GENERIC_LENS_EVIDENCE] },\n components: [\n { ...identity, id: \"distortion\", kind: \"geometry\", residualNote: \"Corrects the lens's radial distortion r_d = r(1 + k1ρ²); the stretched corners lose resolution.\",\n transform: { id: lens.lensId + \":distortion\", version: \"1.0.0\", domain, purpose: \"distortion\", kind: \"radial\",\n profile: { normalizationRadiusMm: lens.halfDiagonalMm, maximumNormalizedRadius: 1.05, coefficients: { k1: c.distortion.corner, k2: 0, k3: 0 } } } },\n { ...identity, id: \"lateral-ca\", kind: \"lateral-ca\", residualNote: \"Registers the channels at one effective wavelength each; colour inside a channel's band and axial colour stay.\",\n transforms: { red: affine(\"red\"), green: affine(\"green\"), blue: affine(\"blue\") } },\n // #584 (component 1.1.0): the gain also undoes the distortion's area factor, so a flat field comes out flat.\n { ...identity, version: \"1.1.0\", id: \"vignetting\", kind: \"peripheral-illumination\",\n residualNote: \"Gain det J/E at the capture's sensor point and the profile's aperture (E the lens's relative illumination, 1/det J its distortion's area factor); it raises corner noise with the signal.\",\n profile: { ...c.vignetting.profile, maximumNormalizedRadius: 1.05 }, strength: 1, distortionAreaFactor: { ...c.distortion.profile, coefficients: { ...c.distortion.profile.coefficients } } }\n ] });\n}\n"]}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { type CalculationResult } from "../core/calculation-result.js";
|
|
12
12
|
import { type IlluminationVignettingProfile } from "../optics/illumination-vignetting.js";
|
|
13
13
|
import { type GenericOpticalEvidence, type OpticalProfileState } from "../optics/profile-contract.js";
|
|
14
|
-
import type
|
|
14
|
+
import { type LensFieldPointMm, type RadialDistortionProfile } from "../optics/radial-distortion.js";
|
|
15
15
|
import { type DigitalGeometricTransform, type GeometricImageDomain, type GeometricRaster, type GeometricResampler, type GeometricSamplingPlan } from "./geometric-transforms.js";
|
|
16
16
|
export type LensCorrectionChannel = "red" | "green" | "blue";
|
|
17
17
|
interface CorrectionComponentIdentity {
|
|
@@ -40,6 +40,14 @@ export interface IlluminationLensCorrection extends CorrectionComponentIdentity
|
|
|
40
40
|
kind: "peripheral-illumination";
|
|
41
41
|
profile: IlluminationVignettingProfile;
|
|
42
42
|
strength: number;
|
|
43
|
+
/**
|
|
44
|
+
* Optional (#584): the capture's radial distortion r_d = r(1 + k1ρ² + k2ρ⁴ + k3ρ⁶), whose area factor 1/det J the capture also
|
|
45
|
+
* multiplied into the irradiance (det J = (1 + k1ρ_i² + k2ρ_i⁴ + k3ρ_i⁶)(1 + 3k1ρ_i² + 5k2ρ_i⁴ + 7k3ρ_i⁶) at the ideal point ρ_i that
|
|
46
|
+
* the lens images to the sensor point). With it, the relative illumination corrected is E(ρ_s)/det J(ρ_i), evaluated at the
|
|
47
|
+
* capture's sensor (source) point, so strength 1 flattens a flat field. It holds whether or not the distortion itself is corrected.
|
|
48
|
+
* Without it the gain is 1/E alone, as before.
|
|
49
|
+
*/
|
|
50
|
+
distortionAreaFactor?: RadialDistortionProfile;
|
|
43
51
|
}
|
|
44
52
|
export type LensCorrectionComponent = GeometricLensCorrection | LateralCaLensCorrection | IlluminationLensCorrection;
|
|
45
53
|
/** A correction profile for one exact lens-and-camera system. Components are listed in the order they are applied. */
|
|
@@ -78,7 +86,9 @@ export declare function resolveLensCorrectionPlan(input: {
|
|
|
78
86
|
}): CalculationResult<ResolvedLensCorrectionPlan>;
|
|
79
87
|
/**
|
|
80
88
|
* Applies a gain after capture and reports the variance before clipping. It does not rewrite
|
|
81
|
-
* the photon or SNR (signal-to-noise ratio) history of the capture.
|
|
89
|
+
* the photon or SNR (signal-to-noise ratio) history of the capture. The gain is T^(−strength), T the relative illumination at
|
|
90
|
+
* `imagePointMm`: E(ρ) of the component's profile, divided by det J(ρ_i) when the component has a `distortionAreaFactor` (#584;
|
|
91
|
+
* then `imagePointMm` is the capture's sensor point, where the capture applied both).
|
|
82
92
|
*/
|
|
83
93
|
export declare function calculatePeripheralIlluminationCorrection(input: {
|
|
84
94
|
component: IlluminationLensCorrection;
|
|
@@ -13,6 +13,7 @@ import { approximationResult } from "../core/calculation-result.js";
|
|
|
13
13
|
import { InvalidScientificInputError } from "../core/validation.js";
|
|
14
14
|
import { calculateIlluminationVignetting } from "../optics/illumination-vignetting.js";
|
|
15
15
|
import { choice, finite, list, parseEvidence, parseOpticalProfileState, positive, record, requireSameState, textValue } from "../optics/profile-contract.js";
|
|
16
|
+
import { calculateInverseRadialSourcePointValue, validateRadialDistortionProfile } from "../optics/radial-distortion.js";
|
|
16
17
|
import { calculateGeometricResampling, calculateGeometricSamplingPlan, parseDigitalGeometricTransform, prepareGeometricMapping, parseGeometricRaster, calculateValidSourceCrop } from "./geometric-transforms.js";
|
|
17
18
|
import { requireInputObject, requireInputKeys } from "../core/record-validation.js";
|
|
18
19
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
@@ -30,7 +31,7 @@ export function parseGenericLensCorrectionProfile(value) {
|
|
|
30
31
|
const r = record(value, ["schemaVersion", "id", "version", "state", "evidence", "components"]);
|
|
31
32
|
const components = list(r.components, 16).map((value) => {
|
|
32
33
|
const c = record(value, ["id", "version", "domain", "availability", "defaultEnabled", "dependencies",
|
|
33
|
-
"requiredByStabilizationModes", "residualNote", "kind", "transform", "transforms", "profile", "strength"]);
|
|
34
|
+
"requiredByStabilizationModes", "residualNote", "kind", "transform", "transforms", "profile", "strength", "distortionAreaFactor"]);
|
|
34
35
|
if (typeof c.defaultEnabled !== "boolean")
|
|
35
36
|
throw new InvalidConfigurationError("defaultEnabled must be boolean.");
|
|
36
37
|
const identity = { id: textValue(c.id), version: textValue(c.version),
|
|
@@ -40,7 +41,7 @@ export function parseGenericLensCorrectionProfile(value) {
|
|
|
40
41
|
requiredByStabilizationModes: list(c.requiredByStabilizationModes, 16).map(textValue), residualNote: textValue(c.residualNote) };
|
|
41
42
|
const kind = choice(c.kind, ["geometry", "lateral-ca", "peripheral-illumination"]);
|
|
42
43
|
if (kind === "geometry") {
|
|
43
|
-
if (c.transforms !== undefined || c.profile !== undefined || c.strength !== undefined)
|
|
44
|
+
if (c.transforms !== undefined || c.profile !== undefined || c.strength !== undefined || c.distortionAreaFactor !== undefined)
|
|
44
45
|
throw new InvalidScientificInputError("Extraneous geometry parameters.");
|
|
45
46
|
const transform = parseDigitalGeometricTransform(c.transform);
|
|
46
47
|
if (transform.domain !== identity.domain || !["distortion", "breathing"].includes(transform.purpose))
|
|
@@ -48,7 +49,7 @@ export function parseGenericLensCorrectionProfile(value) {
|
|
|
48
49
|
return { ...identity, kind, transform };
|
|
49
50
|
}
|
|
50
51
|
if (kind === "lateral-ca") {
|
|
51
|
-
if (c.transform !== undefined || c.profile !== undefined || c.strength !== undefined)
|
|
52
|
+
if (c.transform !== undefined || c.profile !== undefined || c.strength !== undefined || c.distortionAreaFactor !== undefined)
|
|
52
53
|
throw new InvalidScientificInputError("Extraneous CA parameters.");
|
|
53
54
|
const t = record(c.transforms, ["red", "green", "blue"]);
|
|
54
55
|
const transforms = { red: parseDigitalGeometricTransform(t.red), green: parseDigitalGeometricTransform(t.green), blue: parseDigitalGeometricTransform(t.blue) };
|
|
@@ -63,7 +64,13 @@ export function parseGenericLensCorrectionProfile(value) {
|
|
|
63
64
|
const strength = finite(c.strength, "strength");
|
|
64
65
|
if (strength < 0 || strength > 1)
|
|
65
66
|
throw new InvalidScientificInputError("Strength must lie in [0,1].");
|
|
66
|
-
|
|
67
|
+
if (c.distortionAreaFactor === undefined)
|
|
68
|
+
return { ...identity, kind, profile: illumination(c.profile), strength };
|
|
69
|
+
const d = record(c.distortionAreaFactor, ["normalizationRadiusMm", "maximumNormalizedRadius", "coefficients"]), k = record(d.coefficients, ["k1", "k2", "k3"]);
|
|
70
|
+
const distortionAreaFactor = { normalizationRadiusMm: positive(d.normalizationRadiusMm, "normalizationRadiusMm"), maximumNormalizedRadius: positive(d.maximumNormalizedRadius, "maximumNormalizedRadius"),
|
|
71
|
+
coefficients: { k1: finite(k.k1, "k1"), k2: finite(k.k2, "k2"), k3: finite(k.k3, "k3") } };
|
|
72
|
+
validateRadialDistortionProfile(distortionAreaFactor);
|
|
73
|
+
return { ...identity, kind, profile: illumination(c.profile), strength, distortionAreaFactor };
|
|
67
74
|
});
|
|
68
75
|
const preceding = new Set(), mappingIds = new Set();
|
|
69
76
|
for (const c of components) {
|
|
@@ -119,7 +126,9 @@ export function resolveLensCorrectionPlan(input) {
|
|
|
119
126
|
}
|
|
120
127
|
/**
|
|
121
128
|
* Applies a gain after capture and reports the variance before clipping. It does not rewrite
|
|
122
|
-
* the photon or SNR (signal-to-noise ratio) history of the capture.
|
|
129
|
+
* the photon or SNR (signal-to-noise ratio) history of the capture. The gain is T^(−strength), T the relative illumination at
|
|
130
|
+
* `imagePointMm`: E(ρ) of the component's profile, divided by det J(ρ_i) when the component has a `distortionAreaFactor` (#584;
|
|
131
|
+
* then `imagePointMm` is the capture's sensor point, where the capture applied both).
|
|
123
132
|
*/
|
|
124
133
|
export function calculatePeripheralIlluminationCorrection(input) {
|
|
125
134
|
requireInputKeys(input, "calculatePeripheralIlluminationCorrection", ["component", "imagePointMm", "signal", "noiseVariance", "clippingLevel"]);
|
|
@@ -131,7 +140,12 @@ export function calculatePeripheralIlluminationCorrection(input) {
|
|
|
131
140
|
const level = positive(input.clippingLevel, "clippingLevel");
|
|
132
141
|
if (variance < 0)
|
|
133
142
|
throw new InvalidScientificInputError("Variance must be nonnegative.");
|
|
134
|
-
|
|
143
|
+
let throughput = calculateIlluminationVignetting({ imagePointMm: input.imagePointMm, profile: component.profile }).value.linearThroughputFactor;
|
|
144
|
+
if (component.distortionAreaFactor !== undefined) {
|
|
145
|
+
const { k1, k2, k3 } = component.distortionAreaFactor.coefficients;
|
|
146
|
+
const p2 = calculateInverseRadialSourcePointValue(input.imagePointMm, validateRadialDistortionProfile(component.distortionAreaFactor), "imagePointMm").sourceNormalizedRadius ** 2;
|
|
147
|
+
throughput /= (1 + p2 * (k1 + p2 * (k2 + p2 * k3))) * (1 + p2 * (3 * k1 + p2 * (5 * k2 + p2 * 7 * k3)));
|
|
148
|
+
}
|
|
135
149
|
const gain = throughput ** (-component.strength), before = signal * gain;
|
|
136
150
|
return approximationResult({ gain, signalBeforeClipping: before, signal: Math.min(level, before),
|
|
137
151
|
noiseVarianceBeforeClipping: variance * gain * gain, clipped: before > level }, "post-capture-peripheral-illumination-gain", "0.1.0", ["Variance describes pre-clip samples only", "Gain does not increase captured photons or restore sensor SNR"]);
|
|
@@ -203,8 +217,9 @@ export function calculateLensCorrectedCapture(input) {
|
|
|
203
217
|
if (signal === null)
|
|
204
218
|
return null;
|
|
205
219
|
const raster = input.destinationRaster, x = index % raster.width, y = Math.floor(index / raster.width);
|
|
220
|
+
// #584: a gain that carries the distortion's area factor is the capture's own, so it is read at the source point.
|
|
206
221
|
const corrected = calculatePeripheralIlluminationCorrection({ component: c,
|
|
207
|
-
imagePointMm: { x: raster.centerMm.x + (x + .5 - raster.width / 2) * raster.pitchMm,
|
|
222
|
+
imagePointMm: c.distortionAreaFactor ? sampling.points[index].sourcePointMm : { x: raster.centerMm.x + (x + .5 - raster.width / 2) * raster.pitchMm,
|
|
208
223
|
y: raster.centerMm.y - (y + .5 - raster.height / 2) * raster.pitchMm },
|
|
209
224
|
signal, noiseVariance: 0, clippingLevel: input.clippingLevel }).value;
|
|
210
225
|
if (corrected.clipped)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lens-corrections.js","sourceRoot":"","sources":["../../src/output/lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic lens corrections applied after capture: distortion, lateral chromatic aberration\n * (CA) and vignetting (corner darkening).\n *\n * It parses correction profiles, resolves which corrections are on for a camera state, and\n * applies them to linear RGB channels. It does not extrapolate beyond a profile and does not\n * claim to restore lost detail.\n * @see docs/DIGITAL_OPTICS_FOUNDATION.md for equations, coordinate/unit conventions, blockers and\n * support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { calculateIlluminationVignetting, type IlluminationVignettingProfile } from \"../optics/illumination-vignetting.js\";\nimport {\n choice, finite, list, parseEvidence, parseOpticalProfileState, positive, record,\n requireSameState, textValue, type GenericOpticalEvidence, type OpticalProfileState\n} from \"../optics/profile-contract.js\";\nimport type { LensFieldPointMm } from \"../optics/radial-distortion.js\";\nimport {\n calculateGeometricResampling, calculateGeometricSamplingPlan, parseDigitalGeometricTransform,\n prepareGeometricMapping, parseGeometricRaster, calculateValidSourceCrop, type DigitalGeometricTransform, type GeometricImageDomain,\n type GeometricRaster, type GeometricResampler, type GeometricSamplingPlan\n} from \"./geometric-transforms.js\";\nimport { requireInputObject, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type LensCorrectionChannel = \"red\" | \"green\" | \"blue\";\ninterface CorrectionComponentIdentity {\n id: string;\n version: string;\n domain: GeometricImageDomain;\n availability: \"toggle\" | \"automatic\" | \"mandatory\";\n defaultEnabled: boolean;\n dependencies: readonly string[];\n requiredByStabilizationModes: readonly string[];\n /** Declared remaining errors and calibration limits. They are not inferred from the optical model tier. */\n residualNote: string;\n}\n/** A distortion correction map, declared separately. It may deliberately leave some physical distortion in place. */\nexport interface GeometricLensCorrection extends CorrectionComponentIdentity {\n kind: \"geometry\";\n transform: DigitalGeometricTransform;\n}\n/** Lateral CA: shifts colour channels into register. It does not correct longitudinal CA or colour shading. */\nexport interface LateralCaLensCorrection extends CorrectionComponentIdentity {\n kind: \"lateral-ca\";\n transforms: Readonly<Record<LensCorrectionChannel, DigitalGeometricTransform>>;\n}\n/** Vignetting correction as a gain applied after capture. A strength below 1 deliberately leaves some falloff. Range 0..1. */\nexport interface IlluminationLensCorrection extends CorrectionComponentIdentity {\n kind: \"peripheral-illumination\";\n profile: IlluminationVignettingProfile;\n strength: number;\n}\nexport type LensCorrectionComponent = GeometricLensCorrection | LateralCaLensCorrection | IlluminationLensCorrection;\n/** A correction profile for one exact lens-and-camera system. Components are listed in the order they are applied. */\nexport interface GenericLensCorrectionProfile {\n schemaVersion: \"0.1.0\";\n id: string;\n version: string;\n state: OpticalProfileState;\n evidence: GenericOpticalEvidence;\n components: readonly LensCorrectionComponent[];\n}\nexport interface ResolvedLensCorrectionPlan {\n profile: GenericLensCorrectionProfile;\n outputKind: \"raw-like\" | \"processed\";\n selectionKind: \"camera-selectable\" | \"reference-bypass\";\n components: readonly { component: LensCorrectionComponent; enabled: boolean;\n reason: \"selected\" | \"default\" | \"mandatory\" | \"stabilization-dependency\" | \"component-dependency\" | \"reference-bypass\" }[];\n /** For now, a RAW-like intent is recorded as metadata only. */\n application: \"metadata-only\" | \"bake-downstream\";\n}\nfunction illumination(value: unknown): IlluminationVignettingProfile {\n const r = record(value, [\"normalizationRadiusMm\", \"maximumNormalizedRadius\", \"coefficients\"]);\n const c = record(r.coefficients, [\"r2\", \"r4\", \"r6\"]);\n const profile = { normalizationRadiusMm: positive(r.normalizationRadiusMm, \"normalizationRadiusMm\"),\n maximumNormalizedRadius: positive(r.maximumNormalizedRadius, \"maximumNormalizedRadius\"),\n coefficients: { r2: finite(c.r2, \"r2\"), r4: finite(c.r4, \"r4\"), r6: finite(c.r6, \"r6\") } };\n calculateIlluminationVignetting({ imagePointMm: { x: 0, y: 0 }, profile });\n return profile;\n}\n/** Strictly parses a generic correction profile. It does not extrapolate and makes no claim to restore lost detail. */\nexport function parseGenericLensCorrectionProfile(value: unknown): GenericLensCorrectionProfile {\n const r = record(value, [\"schemaVersion\", \"id\", \"version\", \"state\", \"evidence\", \"components\"]);\n const components = list(r.components, 16).map((value): LensCorrectionComponent => {\n const c = record(value, [\"id\", \"version\", \"domain\", \"availability\", \"defaultEnabled\", \"dependencies\",\n \"requiredByStabilizationModes\", \"residualNote\", \"kind\", \"transform\", \"transforms\", \"profile\", \"strength\"]);\n if (typeof c.defaultEnabled !== \"boolean\") throw new InvalidConfigurationError(\"defaultEnabled must be boolean.\");\n const identity = { id: textValue(c.id), version: textValue(c.version),\n domain: choice(c.domain, [\"raw-channel\", \"reconstructed-linear\", \"processed-output\"]),\n availability: choice(c.availability, [\"toggle\", \"automatic\", \"mandatory\"]), defaultEnabled: c.defaultEnabled,\n dependencies: list(c.dependencies, 16).map(textValue),\n requiredByStabilizationModes: list(c.requiredByStabilizationModes, 16).map(textValue), residualNote: textValue(c.residualNote) };\n const kind = choice(c.kind, [\"geometry\", \"lateral-ca\", \"peripheral-illumination\"]);\n if (kind === \"geometry\") {\n if (c.transforms !== undefined || c.profile !== undefined || c.strength !== undefined) throw new InvalidScientificInputError(\"Extraneous geometry parameters.\");\n const transform = parseDigitalGeometricTransform(c.transform);\n if (transform.domain !== identity.domain || ![\"distortion\", \"breathing\"].includes(transform.purpose)) throw new InvalidScientificInputError(\"Geometry domain/purpose mismatch.\");\n return { ...identity, kind, transform };\n }\n if (kind === \"lateral-ca\") {\n if (c.transform !== undefined || c.profile !== undefined || c.strength !== undefined) throw new InvalidScientificInputError(\"Extraneous CA parameters.\");\n const t = record(c.transforms, [\"red\", \"green\", \"blue\"]);\n const transforms = { red: parseDigitalGeometricTransform(t.red), green: parseDigitalGeometricTransform(t.green), blue: parseDigitalGeometricTransform(t.blue) };\n for (const transform of Object.values(transforms)) {\n if (transform.domain !== identity.domain || transform.purpose !== \"lateral-ca\") throw new InvalidScientificInputError(\"CA domain/purpose mismatch.\");\n }\n return { ...identity, kind, transforms };\n }\n if (c.transform !== undefined || c.transforms !== undefined) throw new InvalidScientificInputError(\"Extraneous illumination geometry.\");\n const strength = finite(c.strength, \"strength\");\n if (strength < 0 || strength > 1) throw new InvalidScientificInputError(\"Strength must lie in [0,1].\");\n return { ...identity, kind, profile: illumination(c.profile), strength };\n });\n const preceding = new Set<string>(), mappingIds = new Set<string>();\n for (const c of components) {\n if (preceding.has(c.id) || c.dependencies.some((id) => !preceding.has(id)) ||\n new Set(c.dependencies).size !== c.dependencies.length) throw new InvalidScientificInputError(\"Component IDs/dependency order must be unique and acyclic.\");\n preceding.add(c.id);\n // Common and per-channel transforms need stable, distinct IDs when they are combined.\n const maps = c.kind === \"geometry\" ? [c.transform] : c.kind === \"lateral-ca\" ? Object.values(c.transforms) : [];\n for (const t of maps) {\n if (mappingIds.has(t.id)) throw new InvalidScientificInputError(\"Duplicate correction transform ID.\");\n mappingIds.add(t.id);\n }\n }\n return { schemaVersion: choice(r.schemaVersion, [\"0.1.0\"]), id: textValue(r.id), version: textValue(r.version),\n state: parseOpticalProfileState(r.state), evidence: parseEvidence(r.evidence), components };\n}\n\n/**\n * Decides which corrections apply for a camera state. It accounts for per-component settings,\n * explicit dependencies between components, RAW intent and the educational \"corrections off\" bypass.\n */\nexport function resolveLensCorrectionPlan(input: {\n profile: GenericLensCorrectionProfile; state: OpticalProfileState;\n selections: Readonly<Record<string, \"on\" | \"off\" | \"auto\">>;\n outputKind: \"raw-like\" | \"processed\"; selectionKind: \"camera-selectable\" | \"reference-bypass\";\n}): CalculationResult<ResolvedLensCorrectionPlan> {\n requireInputKeys(input, \"resolveLensCorrectionPlan\", [\"profile\", \"state\", \"selections\", \"outputKind\", \"selectionKind\"]);\n const profile = parseGenericLensCorrectionProfile(input.profile);\n requireSameState(profile.state, input.state);\n const outputKind = choice(input.outputKind, [\"raw-like\", \"processed\"]);\n const selectionKind = choice(input.selectionKind, [\"camera-selectable\", \"reference-bypass\"]);\n const selections = record(input.selections, profile.components.map((c) => c.id));\n const components: ResolvedLensCorrectionPlan[\"components\"][number][] = profile.components.map((c): ResolvedLensCorrectionPlan[\"components\"][number] => {\n const selection = choice(selections[c.id] ?? \"auto\", [\"on\", \"off\", \"auto\"]);\n if (selectionKind === \"reference-bypass\") return { component: c, enabled: false, reason: \"reference-bypass\" };\n const stabilization = c.requiredByStabilizationModes.includes(profile.state.stabilizationMode);\n if (c.availability === \"mandatory\" && selection === \"off\") throw new InvalidScientificInputError(\"Mandatory correction cannot be disabled as a camera setting; use reference-bypass.\");\n if (c.availability === \"automatic\" && selection !== \"auto\") throw new InvalidScientificInputError(\"Automatic correction has no camera toggle.\");\n return { component: c, enabled: c.availability === \"mandatory\" || stabilization || (selection === \"auto\" ? c.defaultEnabled : selection === \"on\"),\n reason: c.availability === \"mandatory\" ? \"mandatory\" : stabilization ? \"stabilization-dependency\" : selection === \"auto\" ? \"default\" : \"selected\" };\n });\n for (const entry of [...components].reverse()) if (entry.enabled) for (const id of entry.component.dependencies) {\n const dependency = components.find((c) => c.component.id === id)!;\n dependency.enabled = true; dependency.reason = \"component-dependency\";\n }\n return approximationResult({ profile, outputKind, selectionKind, components,\n application: outputKind === \"raw-like\" ? \"metadata-only\" : \"bake-downstream\" },\n \"generic-camera-lens-correction-plan\", \"0.1.0\", [profile.evidence.basis, profile.evidence.residualNote,\n \"Corrections occur after the same physical capture; tier is not a correction-strength input\",\n \"Raw-like output preserves intent without baking corrections; color shading, axial CA and restoration remain unsupported\"]);\n}\n\n/**\n * Applies a gain after capture and reports the variance before clipping. It does not rewrite\n * the photon or SNR (signal-to-noise ratio) history of the capture.\n */\nexport function calculatePeripheralIlluminationCorrection(input: {\n component: IlluminationLensCorrection; imagePointMm: LensFieldPointMm;\n signal: number; noiseVariance: number; clippingLevel: number;\n}): CalculationResult<{ gain: number; signalBeforeClipping: number; signal: number; noiseVarianceBeforeClipping: number; clipped: boolean }> {\n requireInputKeys(input, \"calculatePeripheralIlluminationCorrection\", [\"component\", \"imagePointMm\", \"signal\", \"noiseVariance\", \"clippingLevel\"]);\n const component = input.component;\n requireInputObject(component, \"component input\");\n if (component.kind !== \"peripheral-illumination\" || component.strength < 0 || component.strength > 1 || !Number.isFinite(component.strength)) throw new InvalidScientificInputError(\"Invalid illumination component.\");\n const signal = finite(input.signal, \"signal\"), variance = finite(input.noiseVariance, \"noiseVariance\");\n const level = positive(input.clippingLevel, \"clippingLevel\");\n if (variance < 0) throw new InvalidScientificInputError(\"Variance must be nonnegative.\");\n const throughput = calculateIlluminationVignetting({ imagePointMm: input.imagePointMm, profile: component.profile }).value.linearThroughputFactor;\n const gain = throughput**(-component.strength), before = signal*gain;\n return approximationResult({ gain, signalBeforeClipping: before, signal: Math.min(level, before),\n noiseVarianceBeforeClipping: variance*gain*gain, clipped: before > level },\n \"post-capture-peripheral-illumination-gain\", \"0.1.0\", [\"Variance describes pre-clip samples only\", \"Gain does not increase captured photons or restore sensor SNR\"]);\n}\n\n/** The fixed capture identity travels with the output channels, so On/Off comparisons use the same capture. */\nexport interface LensCorrectionCapture {\n state: OpticalProfileState;\n captureId: string;\n noiseRealizationId: string;\n timeSeconds: number;\n raster: GeometricRaster;\n channels: Readonly<Record<LensCorrectionChannel, readonly number[]>>;\n}\n/** Applies bounded corrections to RGB channels that are already sampled and linear.\n * Compatible shared geometry and per-channel CA are combined, so each channel is resampled once.\n * A gain between geometry components, or incompatible domains, needs a separate staged caller.\n */\nexport function calculateLensCorrectedCapture(input: {\n plan: ResolvedLensCorrectionPlan; capture: LensCorrectionCapture;\n destinationRaster: GeometricRaster; resampler: GeometricResampler;\n physicalProjectionDistanceMm: number; clippingLevel: number;\n}): CalculationResult<{\n captureId: string; noiseRealizationId: string; timeSeconds: number;\n application: ResolvedLensCorrectionPlan[\"application\"];\n channels: Readonly<Record<LensCorrectionChannel, readonly (number | null)[]>>;\n samplingPlans: Partial<Record<LensCorrectionChannel, GeometricSamplingPlan>>;\n /** Area valid in every channel, before cropping. Missing colours are never made up. */\n validSourceMask: readonly boolean[];\n jointCrop: GeometricSamplingPlan[\"jointCrop\"];\n /** Clipping events in gain stages, before the shared valid-area crop. Each stage is counted separately. */\n illuminationClippingEventCount: number;\n}> {\n requireInputKeys(input, \"calculateLensCorrectedCapture\", [\"plan\", \"capture\", \"destinationRaster\", \"resampler\", \"physicalProjectionDistanceMm\", \"clippingLevel\"]);\n // Re-resolve the plan, which may be untrusted or changed, so callers cannot fake enabled or mandatory states.\n requireInputObject(input.plan, \"plan\");\n const plan = resolveLensCorrectionPlan({ profile: input.plan.profile, state: input.plan.profile.state,\n selections: Object.fromEntries(input.plan.components.filter((e) => e.component.availability === \"toggle\").map((e) => [e.component.id, e.enabled ? \"on\" : \"off\"])),\n outputKind: input.plan.outputKind, selectionKind: input.plan.selectionKind }).value;\n const capture = input.capture;\n requireSameState(plan.profile.state, capture.state);\n const sourceRaster = parseGeometricRaster(capture.raster);\n const destination = parseGeometricRaster(input.destinationRaster);\n if (destination.width !== plan.profile.state.outputWidth || destination.height !== plan.profile.state.outputHeight) {\n throw new InvalidScientificInputError(\"Destination raster differs from correction output binding.\");\n }\n textValue(capture.captureId); textValue(capture.noiseRealizationId);\n if (finite(capture.timeSeconds, \"timeSeconds\") < 0) throw new InvalidScientificInputError(\"Invalid capture time.\");\n const count = capture.raster.width*capture.raster.height;\n for (const channel of [\"red\", \"green\", \"blue\"] as const) {\n const data = list(capture.channels[channel], 65536);\n if (data.length !== count) throw new InvalidScientificInputError(\"Capture channel sample count mismatch.\");\n data.forEach((s) => finite(s, \"channel sample\"));\n }\n const active = plan.components.filter((e) => e.enabled).map((e) => e.component);\n const sameLattice = sourceRaster.width === destination.width && sourceRaster.height === destination.height &&\n sourceRaster.pitchMm === destination.pitchMm && sourceRaster.centerMm.x === destination.centerMm.x &&\n sourceRaster.centerMm.y === destination.centerMm.y;\n // Disabling corrections does not disable an explicit output crop/resampling request.\n if (plan.application === \"metadata-only\" || (!active.length && sameLattice)) {\n return approximationResult({ captureId: capture.captureId, noiseRealizationId: capture.noiseRealizationId,\n timeSeconds: capture.timeSeconds, application: plan.application, channels: capture.channels,\n samplingPlans: {}, validSourceMask: Array<boolean>(count).fill(true), illuminationClippingEventCount: 0,\n jointCrop: { x: 0, y: 0, width: capture.raster.width, height: capture.raster.height } },\n \"generic-lens-correction-capture\", \"0.1.0\", [\"Original physical capture preserved\"]);\n }\n if (active.some((c) => c.domain !== \"reconstructed-linear\")) throw new InvalidScientificInputError(\"This executor requires reconstructed-linear components; other domains need separate consumers.\");\n let seenGain = false;\n for (const c of active) {\n if (c.kind === \"peripheral-illumination\") seenGain = true;\n else if (seenGain) throw new InvalidScientificInputError(\"Cannot reorder geometry across a gain stage.\");\n }\n const channels = {} as Record<LensCorrectionChannel, readonly (number | null)[]>;\n const samplingPlans: Partial<Record<LensCorrectionChannel, GeometricSamplingPlan>> = {};\n let illuminationClippingEventCount = 0;\n for (const channel of [\"red\", \"green\", \"blue\"] as const) {\n // Corrections are listed source → destination. Inverse sampling runs them in reverse.\n const transforms = active.filter((c) => c.kind !== \"peripheral-illumination\").reverse().map((c) =>\n c.kind === \"geometry\" ? c.transform : (c as LateralCaLensCorrection).transforms[channel]);\n const mapping = prepareGeometricMapping({ transforms, frameTimeSeconds: capture.timeSeconds }).value;\n const sampling = calculateGeometricSamplingPlan({ mapping, sourceRaster: capture.raster,\n destinationRaster: input.destinationRaster, resampler: input.resampler,\n physicalProjectionDistanceMm: input.physicalProjectionDistanceMm }).value;\n samplingPlans[channel] = sampling;\n let data = calculateGeometricResampling({ plan: sampling, sourceSamples: capture.channels[channel] }).value.samples;\n for (const c of active) if (c.kind === \"peripheral-illumination\") {\n data = data.map((signal, index): number | null => {\n if (signal === null) return null;\n const raster = input.destinationRaster, x = index%raster.width, y = Math.floor(index/raster.width);\n const corrected = calculatePeripheralIlluminationCorrection({ component: c,\n imagePointMm: { x: raster.centerMm.x+(x+.5-raster.width/2)*raster.pitchMm,\n y: raster.centerMm.y-(y+.5-raster.height/2)*raster.pitchMm },\n signal, noiseVariance: 0, clippingLevel: input.clippingLevel }).value;\n if (corrected.clipped) illuminationClippingEventCount++;\n return corrected.signal;\n });\n }\n channels[channel] = data;\n }\n const mask = channels.red.map((_, i) => channels.red[i] !== null && channels.green[i] !== null && channels.blue[i] !== null);\n for (const channel of [\"red\", \"green\", \"blue\"] as const) channels[channel] = channels[channel].map((s, i) => mask[i] ? s : null);\n return approximationResult({ captureId: capture.captureId, noiseRealizationId: capture.noiseRealizationId,\n timeSeconds: capture.timeSeconds, application: plan.application, channels, samplingPlans, validSourceMask: mask,\n illuminationClippingEventCount,\n jointCrop: calculateValidSourceCrop(mask, destination.width, destination.height) },\n \"generic-lens-correction-capture\", \"0.1.0\", [\"Input capture/noise realization remains unchanged\", \"Channel support intersects before output; optical blur/noise is warped, not physically repaired\",\n \"No production-plan composition is enabled by this standalone executor\"]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"lens-corrections.js","sourceRoot":"","sources":["../../src/output/lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic lens corrections applied after capture: distortion, lateral chromatic aberration\n * (CA) and vignetting (corner darkening).\n *\n * It parses correction profiles, resolves which corrections are on for a camera state, and\n * applies them to linear RGB channels. It does not extrapolate beyond a profile and does not\n * claim to restore lost detail.\n * @see docs/DIGITAL_OPTICS_FOUNDATION.md for equations, coordinate/unit conventions, blockers and\n * support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { calculateIlluminationVignetting, type IlluminationVignettingProfile } from \"../optics/illumination-vignetting.js\";\nimport {\n choice, finite, list, parseEvidence, parseOpticalProfileState, positive, record,\n requireSameState, textValue, type GenericOpticalEvidence, type OpticalProfileState\n} from \"../optics/profile-contract.js\";\nimport { calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type LensFieldPointMm, type RadialDistortionProfile } from \"../optics/radial-distortion.js\";\nimport {\n calculateGeometricResampling, calculateGeometricSamplingPlan, parseDigitalGeometricTransform,\n prepareGeometricMapping, parseGeometricRaster, calculateValidSourceCrop, type DigitalGeometricTransform, type GeometricImageDomain,\n type GeometricRaster, type GeometricResampler, type GeometricSamplingPlan\n} from \"./geometric-transforms.js\";\nimport { requireInputObject, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type LensCorrectionChannel = \"red\" | \"green\" | \"blue\";\ninterface CorrectionComponentIdentity {\n id: string;\n version: string;\n domain: GeometricImageDomain;\n availability: \"toggle\" | \"automatic\" | \"mandatory\";\n defaultEnabled: boolean;\n dependencies: readonly string[];\n requiredByStabilizationModes: readonly string[];\n /** Declared remaining errors and calibration limits. They are not inferred from the optical model tier. */\n residualNote: string;\n}\n/** A distortion correction map, declared separately. It may deliberately leave some physical distortion in place. */\nexport interface GeometricLensCorrection extends CorrectionComponentIdentity {\n kind: \"geometry\";\n transform: DigitalGeometricTransform;\n}\n/** Lateral CA: shifts colour channels into register. It does not correct longitudinal CA or colour shading. */\nexport interface LateralCaLensCorrection extends CorrectionComponentIdentity {\n kind: \"lateral-ca\";\n transforms: Readonly<Record<LensCorrectionChannel, DigitalGeometricTransform>>;\n}\n/** Vignetting correction as a gain applied after capture. A strength below 1 deliberately leaves some falloff. Range 0..1. */\nexport interface IlluminationLensCorrection extends CorrectionComponentIdentity {\n kind: \"peripheral-illumination\";\n profile: IlluminationVignettingProfile;\n strength: number;\n /**\n * Optional (#584): the capture's radial distortion r_d = r(1 + k1ρ² + k2ρ⁴ + k3ρ⁶), whose area factor 1/det J the capture also\n * multiplied into the irradiance (det J = (1 + k1ρ_i² + k2ρ_i⁴ + k3ρ_i⁶)(1 + 3k1ρ_i² + 5k2ρ_i⁴ + 7k3ρ_i⁶) at the ideal point ρ_i that\n * the lens images to the sensor point). With it, the relative illumination corrected is E(ρ_s)/det J(ρ_i), evaluated at the\n * capture's sensor (source) point, so strength 1 flattens a flat field. It holds whether or not the distortion itself is corrected.\n * Without it the gain is 1/E alone, as before.\n */\n distortionAreaFactor?: RadialDistortionProfile;\n}\nexport type LensCorrectionComponent = GeometricLensCorrection | LateralCaLensCorrection | IlluminationLensCorrection;\n/** A correction profile for one exact lens-and-camera system. Components are listed in the order they are applied. */\nexport interface GenericLensCorrectionProfile {\n schemaVersion: \"0.1.0\";\n id: string;\n version: string;\n state: OpticalProfileState;\n evidence: GenericOpticalEvidence;\n components: readonly LensCorrectionComponent[];\n}\nexport interface ResolvedLensCorrectionPlan {\n profile: GenericLensCorrectionProfile;\n outputKind: \"raw-like\" | \"processed\";\n selectionKind: \"camera-selectable\" | \"reference-bypass\";\n components: readonly { component: LensCorrectionComponent; enabled: boolean;\n reason: \"selected\" | \"default\" | \"mandatory\" | \"stabilization-dependency\" | \"component-dependency\" | \"reference-bypass\" }[];\n /** For now, a RAW-like intent is recorded as metadata only. */\n application: \"metadata-only\" | \"bake-downstream\";\n}\nfunction illumination(value: unknown): IlluminationVignettingProfile {\n const r = record(value, [\"normalizationRadiusMm\", \"maximumNormalizedRadius\", \"coefficients\"]);\n const c = record(r.coefficients, [\"r2\", \"r4\", \"r6\"]);\n const profile = { normalizationRadiusMm: positive(r.normalizationRadiusMm, \"normalizationRadiusMm\"),\n maximumNormalizedRadius: positive(r.maximumNormalizedRadius, \"maximumNormalizedRadius\"),\n coefficients: { r2: finite(c.r2, \"r2\"), r4: finite(c.r4, \"r4\"), r6: finite(c.r6, \"r6\") } };\n calculateIlluminationVignetting({ imagePointMm: { x: 0, y: 0 }, profile });\n return profile;\n}\n/** Strictly parses a generic correction profile. It does not extrapolate and makes no claim to restore lost detail. */\nexport function parseGenericLensCorrectionProfile(value: unknown): GenericLensCorrectionProfile {\n const r = record(value, [\"schemaVersion\", \"id\", \"version\", \"state\", \"evidence\", \"components\"]);\n const components = list(r.components, 16).map((value): LensCorrectionComponent => {\n const c = record(value, [\"id\", \"version\", \"domain\", \"availability\", \"defaultEnabled\", \"dependencies\",\n \"requiredByStabilizationModes\", \"residualNote\", \"kind\", \"transform\", \"transforms\", \"profile\", \"strength\", \"distortionAreaFactor\"]);\n if (typeof c.defaultEnabled !== \"boolean\") throw new InvalidConfigurationError(\"defaultEnabled must be boolean.\");\n const identity = { id: textValue(c.id), version: textValue(c.version),\n domain: choice(c.domain, [\"raw-channel\", \"reconstructed-linear\", \"processed-output\"]),\n availability: choice(c.availability, [\"toggle\", \"automatic\", \"mandatory\"]), defaultEnabled: c.defaultEnabled,\n dependencies: list(c.dependencies, 16).map(textValue),\n requiredByStabilizationModes: list(c.requiredByStabilizationModes, 16).map(textValue), residualNote: textValue(c.residualNote) };\n const kind = choice(c.kind, [\"geometry\", \"lateral-ca\", \"peripheral-illumination\"]);\n if (kind === \"geometry\") {\n if (c.transforms !== undefined || c.profile !== undefined || c.strength !== undefined || c.distortionAreaFactor !== undefined) throw new InvalidScientificInputError(\"Extraneous geometry parameters.\");\n const transform = parseDigitalGeometricTransform(c.transform);\n if (transform.domain !== identity.domain || ![\"distortion\", \"breathing\"].includes(transform.purpose)) throw new InvalidScientificInputError(\"Geometry domain/purpose mismatch.\");\n return { ...identity, kind, transform };\n }\n if (kind === \"lateral-ca\") {\n if (c.transform !== undefined || c.profile !== undefined || c.strength !== undefined || c.distortionAreaFactor !== undefined) throw new InvalidScientificInputError(\"Extraneous CA parameters.\");\n const t = record(c.transforms, [\"red\", \"green\", \"blue\"]);\n const transforms = { red: parseDigitalGeometricTransform(t.red), green: parseDigitalGeometricTransform(t.green), blue: parseDigitalGeometricTransform(t.blue) };\n for (const transform of Object.values(transforms)) {\n if (transform.domain !== identity.domain || transform.purpose !== \"lateral-ca\") throw new InvalidScientificInputError(\"CA domain/purpose mismatch.\");\n }\n return { ...identity, kind, transforms };\n }\n if (c.transform !== undefined || c.transforms !== undefined) throw new InvalidScientificInputError(\"Extraneous illumination geometry.\");\n const strength = finite(c.strength, \"strength\");\n if (strength < 0 || strength > 1) throw new InvalidScientificInputError(\"Strength must lie in [0,1].\");\n if (c.distortionAreaFactor === undefined) return { ...identity, kind, profile: illumination(c.profile), strength };\n const d = record(c.distortionAreaFactor, [\"normalizationRadiusMm\", \"maximumNormalizedRadius\", \"coefficients\"]), k = record(d.coefficients, [\"k1\", \"k2\", \"k3\"]);\n const distortionAreaFactor = { normalizationRadiusMm: positive(d.normalizationRadiusMm, \"normalizationRadiusMm\"), maximumNormalizedRadius: positive(d.maximumNormalizedRadius, \"maximumNormalizedRadius\"),\n coefficients: { k1: finite(k.k1, \"k1\"), k2: finite(k.k2, \"k2\"), k3: finite(k.k3, \"k3\") } };\n validateRadialDistortionProfile(distortionAreaFactor);\n return { ...identity, kind, profile: illumination(c.profile), strength, distortionAreaFactor };\n });\n const preceding = new Set<string>(), mappingIds = new Set<string>();\n for (const c of components) {\n if (preceding.has(c.id) || c.dependencies.some((id) => !preceding.has(id)) ||\n new Set(c.dependencies).size !== c.dependencies.length) throw new InvalidScientificInputError(\"Component IDs/dependency order must be unique and acyclic.\");\n preceding.add(c.id);\n // Common and per-channel transforms need stable, distinct IDs when they are combined.\n const maps = c.kind === \"geometry\" ? [c.transform] : c.kind === \"lateral-ca\" ? Object.values(c.transforms) : [];\n for (const t of maps) {\n if (mappingIds.has(t.id)) throw new InvalidScientificInputError(\"Duplicate correction transform ID.\");\n mappingIds.add(t.id);\n }\n }\n return { schemaVersion: choice(r.schemaVersion, [\"0.1.0\"]), id: textValue(r.id), version: textValue(r.version),\n state: parseOpticalProfileState(r.state), evidence: parseEvidence(r.evidence), components };\n}\n\n/**\n * Decides which corrections apply for a camera state. It accounts for per-component settings,\n * explicit dependencies between components, RAW intent and the educational \"corrections off\" bypass.\n */\nexport function resolveLensCorrectionPlan(input: {\n profile: GenericLensCorrectionProfile; state: OpticalProfileState;\n selections: Readonly<Record<string, \"on\" | \"off\" | \"auto\">>;\n outputKind: \"raw-like\" | \"processed\"; selectionKind: \"camera-selectable\" | \"reference-bypass\";\n}): CalculationResult<ResolvedLensCorrectionPlan> {\n requireInputKeys(input, \"resolveLensCorrectionPlan\", [\"profile\", \"state\", \"selections\", \"outputKind\", \"selectionKind\"]);\n const profile = parseGenericLensCorrectionProfile(input.profile);\n requireSameState(profile.state, input.state);\n const outputKind = choice(input.outputKind, [\"raw-like\", \"processed\"]);\n const selectionKind = choice(input.selectionKind, [\"camera-selectable\", \"reference-bypass\"]);\n const selections = record(input.selections, profile.components.map((c) => c.id));\n const components: ResolvedLensCorrectionPlan[\"components\"][number][] = profile.components.map((c): ResolvedLensCorrectionPlan[\"components\"][number] => {\n const selection = choice(selections[c.id] ?? \"auto\", [\"on\", \"off\", \"auto\"]);\n if (selectionKind === \"reference-bypass\") return { component: c, enabled: false, reason: \"reference-bypass\" };\n const stabilization = c.requiredByStabilizationModes.includes(profile.state.stabilizationMode);\n if (c.availability === \"mandatory\" && selection === \"off\") throw new InvalidScientificInputError(\"Mandatory correction cannot be disabled as a camera setting; use reference-bypass.\");\n if (c.availability === \"automatic\" && selection !== \"auto\") throw new InvalidScientificInputError(\"Automatic correction has no camera toggle.\");\n return { component: c, enabled: c.availability === \"mandatory\" || stabilization || (selection === \"auto\" ? c.defaultEnabled : selection === \"on\"),\n reason: c.availability === \"mandatory\" ? \"mandatory\" : stabilization ? \"stabilization-dependency\" : selection === \"auto\" ? \"default\" : \"selected\" };\n });\n for (const entry of [...components].reverse()) if (entry.enabled) for (const id of entry.component.dependencies) {\n const dependency = components.find((c) => c.component.id === id)!;\n dependency.enabled = true; dependency.reason = \"component-dependency\";\n }\n return approximationResult({ profile, outputKind, selectionKind, components,\n application: outputKind === \"raw-like\" ? \"metadata-only\" : \"bake-downstream\" },\n \"generic-camera-lens-correction-plan\", \"0.1.0\", [profile.evidence.basis, profile.evidence.residualNote,\n \"Corrections occur after the same physical capture; tier is not a correction-strength input\",\n \"Raw-like output preserves intent without baking corrections; color shading, axial CA and restoration remain unsupported\"]);\n}\n\n/**\n * Applies a gain after capture and reports the variance before clipping. It does not rewrite\n * the photon or SNR (signal-to-noise ratio) history of the capture. The gain is T^(−strength), T the relative illumination at\n * `imagePointMm`: E(ρ) of the component's profile, divided by det J(ρ_i) when the component has a `distortionAreaFactor` (#584;\n * then `imagePointMm` is the capture's sensor point, where the capture applied both).\n */\nexport function calculatePeripheralIlluminationCorrection(input: {\n component: IlluminationLensCorrection; imagePointMm: LensFieldPointMm;\n signal: number; noiseVariance: number; clippingLevel: number;\n}): CalculationResult<{ gain: number; signalBeforeClipping: number; signal: number; noiseVarianceBeforeClipping: number; clipped: boolean }> {\n requireInputKeys(input, \"calculatePeripheralIlluminationCorrection\", [\"component\", \"imagePointMm\", \"signal\", \"noiseVariance\", \"clippingLevel\"]);\n const component = input.component;\n requireInputObject(component, \"component input\");\n if (component.kind !== \"peripheral-illumination\" || component.strength < 0 || component.strength > 1 || !Number.isFinite(component.strength)) throw new InvalidScientificInputError(\"Invalid illumination component.\");\n const signal = finite(input.signal, \"signal\"), variance = finite(input.noiseVariance, \"noiseVariance\");\n const level = positive(input.clippingLevel, \"clippingLevel\");\n if (variance < 0) throw new InvalidScientificInputError(\"Variance must be nonnegative.\");\n let throughput = calculateIlluminationVignetting({ imagePointMm: input.imagePointMm, profile: component.profile }).value.linearThroughputFactor;\n if (component.distortionAreaFactor !== undefined) {\n const { k1, k2, k3 } = component.distortionAreaFactor.coefficients;\n const p2 = calculateInverseRadialSourcePointValue(input.imagePointMm, validateRadialDistortionProfile(component.distortionAreaFactor), \"imagePointMm\").sourceNormalizedRadius ** 2;\n throughput /= (1 + p2 * (k1 + p2 * (k2 + p2 * k3))) * (1 + p2 * (3 * k1 + p2 * (5 * k2 + p2 * 7 * k3)));\n }\n const gain = throughput**(-component.strength), before = signal*gain;\n return approximationResult({ gain, signalBeforeClipping: before, signal: Math.min(level, before),\n noiseVarianceBeforeClipping: variance*gain*gain, clipped: before > level },\n \"post-capture-peripheral-illumination-gain\", \"0.1.0\", [\"Variance describes pre-clip samples only\", \"Gain does not increase captured photons or restore sensor SNR\"]);\n}\n\n/** The fixed capture identity travels with the output channels, so On/Off comparisons use the same capture. */\nexport interface LensCorrectionCapture {\n state: OpticalProfileState;\n captureId: string;\n noiseRealizationId: string;\n timeSeconds: number;\n raster: GeometricRaster;\n channels: Readonly<Record<LensCorrectionChannel, readonly number[]>>;\n}\n/** Applies bounded corrections to RGB channels that are already sampled and linear.\n * Compatible shared geometry and per-channel CA are combined, so each channel is resampled once.\n * A gain between geometry components, or incompatible domains, needs a separate staged caller.\n */\nexport function calculateLensCorrectedCapture(input: {\n plan: ResolvedLensCorrectionPlan; capture: LensCorrectionCapture;\n destinationRaster: GeometricRaster; resampler: GeometricResampler;\n physicalProjectionDistanceMm: number; clippingLevel: number;\n}): CalculationResult<{\n captureId: string; noiseRealizationId: string; timeSeconds: number;\n application: ResolvedLensCorrectionPlan[\"application\"];\n channels: Readonly<Record<LensCorrectionChannel, readonly (number | null)[]>>;\n samplingPlans: Partial<Record<LensCorrectionChannel, GeometricSamplingPlan>>;\n /** Area valid in every channel, before cropping. Missing colours are never made up. */\n validSourceMask: readonly boolean[];\n jointCrop: GeometricSamplingPlan[\"jointCrop\"];\n /** Clipping events in gain stages, before the shared valid-area crop. Each stage is counted separately. */\n illuminationClippingEventCount: number;\n}> {\n requireInputKeys(input, \"calculateLensCorrectedCapture\", [\"plan\", \"capture\", \"destinationRaster\", \"resampler\", \"physicalProjectionDistanceMm\", \"clippingLevel\"]);\n // Re-resolve the plan, which may be untrusted or changed, so callers cannot fake enabled or mandatory states.\n requireInputObject(input.plan, \"plan\");\n const plan = resolveLensCorrectionPlan({ profile: input.plan.profile, state: input.plan.profile.state,\n selections: Object.fromEntries(input.plan.components.filter((e) => e.component.availability === \"toggle\").map((e) => [e.component.id, e.enabled ? \"on\" : \"off\"])),\n outputKind: input.plan.outputKind, selectionKind: input.plan.selectionKind }).value;\n const capture = input.capture;\n requireSameState(plan.profile.state, capture.state);\n const sourceRaster = parseGeometricRaster(capture.raster);\n const destination = parseGeometricRaster(input.destinationRaster);\n if (destination.width !== plan.profile.state.outputWidth || destination.height !== plan.profile.state.outputHeight) {\n throw new InvalidScientificInputError(\"Destination raster differs from correction output binding.\");\n }\n textValue(capture.captureId); textValue(capture.noiseRealizationId);\n if (finite(capture.timeSeconds, \"timeSeconds\") < 0) throw new InvalidScientificInputError(\"Invalid capture time.\");\n const count = capture.raster.width*capture.raster.height;\n for (const channel of [\"red\", \"green\", \"blue\"] as const) {\n const data = list(capture.channels[channel], 65536);\n if (data.length !== count) throw new InvalidScientificInputError(\"Capture channel sample count mismatch.\");\n data.forEach((s) => finite(s, \"channel sample\"));\n }\n const active = plan.components.filter((e) => e.enabled).map((e) => e.component);\n const sameLattice = sourceRaster.width === destination.width && sourceRaster.height === destination.height &&\n sourceRaster.pitchMm === destination.pitchMm && sourceRaster.centerMm.x === destination.centerMm.x &&\n sourceRaster.centerMm.y === destination.centerMm.y;\n // Disabling corrections does not disable an explicit output crop/resampling request.\n if (plan.application === \"metadata-only\" || (!active.length && sameLattice)) {\n return approximationResult({ captureId: capture.captureId, noiseRealizationId: capture.noiseRealizationId,\n timeSeconds: capture.timeSeconds, application: plan.application, channels: capture.channels,\n samplingPlans: {}, validSourceMask: Array<boolean>(count).fill(true), illuminationClippingEventCount: 0,\n jointCrop: { x: 0, y: 0, width: capture.raster.width, height: capture.raster.height } },\n \"generic-lens-correction-capture\", \"0.1.0\", [\"Original physical capture preserved\"]);\n }\n if (active.some((c) => c.domain !== \"reconstructed-linear\")) throw new InvalidScientificInputError(\"This executor requires reconstructed-linear components; other domains need separate consumers.\");\n let seenGain = false;\n for (const c of active) {\n if (c.kind === \"peripheral-illumination\") seenGain = true;\n else if (seenGain) throw new InvalidScientificInputError(\"Cannot reorder geometry across a gain stage.\");\n }\n const channels = {} as Record<LensCorrectionChannel, readonly (number | null)[]>;\n const samplingPlans: Partial<Record<LensCorrectionChannel, GeometricSamplingPlan>> = {};\n let illuminationClippingEventCount = 0;\n for (const channel of [\"red\", \"green\", \"blue\"] as const) {\n // Corrections are listed source → destination. Inverse sampling runs them in reverse.\n const transforms = active.filter((c) => c.kind !== \"peripheral-illumination\").reverse().map((c) =>\n c.kind === \"geometry\" ? c.transform : (c as LateralCaLensCorrection).transforms[channel]);\n const mapping = prepareGeometricMapping({ transforms, frameTimeSeconds: capture.timeSeconds }).value;\n const sampling = calculateGeometricSamplingPlan({ mapping, sourceRaster: capture.raster,\n destinationRaster: input.destinationRaster, resampler: input.resampler,\n physicalProjectionDistanceMm: input.physicalProjectionDistanceMm }).value;\n samplingPlans[channel] = sampling;\n let data = calculateGeometricResampling({ plan: sampling, sourceSamples: capture.channels[channel] }).value.samples;\n for (const c of active) if (c.kind === \"peripheral-illumination\") {\n data = data.map((signal, index): number | null => {\n if (signal === null) return null;\n const raster = input.destinationRaster, x = index%raster.width, y = Math.floor(index/raster.width);\n // #584: a gain that carries the distortion's area factor is the capture's own, so it is read at the source point.\n const corrected = calculatePeripheralIlluminationCorrection({ component: c,\n imagePointMm: c.distortionAreaFactor ? sampling.points[index]!.sourcePointMm : { x: raster.centerMm.x+(x+.5-raster.width/2)*raster.pitchMm,\n y: raster.centerMm.y-(y+.5-raster.height/2)*raster.pitchMm },\n signal, noiseVariance: 0, clippingLevel: input.clippingLevel }).value;\n if (corrected.clipped) illuminationClippingEventCount++;\n return corrected.signal;\n });\n }\n channels[channel] = data;\n }\n const mask = channels.red.map((_, i) => channels.red[i] !== null && channels.green[i] !== null && channels.blue[i] !== null);\n for (const channel of [\"red\", \"green\", \"blue\"] as const) channels[channel] = channels[channel].map((s, i) => mask[i] ? s : null);\n return approximationResult({ captureId: capture.captureId, noiseRealizationId: capture.noiseRealizationId,\n timeSeconds: capture.timeSeconds, application: plan.application, channels, samplingPlans, validSourceMask: mask,\n illuminationClippingEventCount,\n jointCrop: calculateValidSourceCrop(mask, destination.width, destination.height) },\n \"generic-lens-correction-capture\", \"0.1.0\", [\"Input capture/noise realization remains unchanged\", \"Channel support intersects before output; optical blur/noise is warped, not physically repaired\",\n \"No production-plan composition is enabled by this standalone executor\"]);\n}\n"]}
|
|
@@ -36,7 +36,7 @@ export function parsePrintContrastReferenceInput(value) {
|
|
|
36
36
|
eccentricityDegrees: number("eccentricityDegrees", false), modulationMichelson };
|
|
37
37
|
}
|
|
38
38
|
// Independently written scalar evaluation of the licensed numerical model.
|
|
39
|
-
// Using log1p/expm1 keeps the small
|
|
39
|
+
// Using log1p/expm1 keeps the hyperbolic terms accurate where the small quantity k_s4/Y (about 5.6e-10 at 1000 cd/m²) would cancel.
|
|
40
40
|
function luminanceDependency(p, luminance) {
|
|
41
41
|
if (p.length === 2)
|
|
42
42
|
return p[1] * luminance ** p[0];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"print-contrast-reference.js","sourceRoot":"","sources":["../../src/output/print-contrast-reference.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// castleCSF numerical model material: Copyright (c) 2023 Graphics and Displays\n// group - University of Cambridge. MIT terms retained in NOTICE (the full\n// licence text). Original bounded TypeScript adapter.\n\n/** Contrast detection threshold for a static, neutral (D65 white) Gabor patch, from the castleCSF\n * contrast sensitivity function (CSF) model. A Gabor patch is a sine grating under a Gaussian envelope.\n * It does not say whether detail in a photograph will be visible.\n * @see docs/PRINT_CONTRAST_REFERENCE.md for exact source, applicability and review gates.\n */\nimport { estimatedResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { freezeOwnedData } from \"../core/owned-data.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId } from \"../core/record-validation.js\";\nimport { ENGINE_API_VERSION } from \"../core/version.js\";\n\nexport const PRINT_CONTRAST_REFERENCE_MODEL_VERSION = \"0.1.0\" as const;\n/** Identifies the exact MIT-licensed model source. It is not a local calibration. */\nexport const PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION = \"f4b0b722af83001d7af979281e06ca642d36e4e8\" as const;\n\n/** The narrow reference stimulus. It is separate from any measured photographic region of interest (ROI).\n * Positive values outside the model's range return \"unsupported\" instead of being extrapolated.\n */\nexport interface PrintContrastReferenceInput {\n referenceId: string;\n stimulus: \"static-neutral-d65-gabor\";\n observer: \"published-binocular-natural-pupil-reference\";\n /** Mean luminance of the uniform D65 background and grating. Not the average of a photographic ROI. */\n meanLuminanceCdPerSquareMeter: number;\n spatialFrequencyCyclesPerDegree: number;\n gaussianEnvelopeSigmaDegrees: number;\n temporalFrequencyHz: number;\n eccentricityDegrees: number;\n /** Fractional amplitude of the neutral grating before the Gaussian envelope. Not taken from ROI minimum and maximum values. */\n modulationMichelson: number;\n}\nexport interface PrintContrastReference {\n referenceId: string;\n engineApiVersion: string;\n modelVersion: typeof PRINT_CONTRAST_REFERENCE_MODEL_VERSION;\n upstreamRevision: typeof PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION;\n input: PrintContrastReferenceInput;\n status: \"model-estimate\" | \"unsupported\";\n blockers: string[];\n estimate: { contrastSensitivity: number; thresholdModulationMichelson: number; suppliedModulationToModelThresholdRatio: number } | null;\n observerApplicability: \"population-reference-unverified-for-individual\";\n naturalImageVisibility: \"unassessed\";\n printStimulusMatch: \"unassessed\";\n uncertainty: \"not-quantified\";\n overallPrintVerdict: \"not-offered\";\n}\n\n/** Strictly parses and copies the explicit reference conditions. No defaults are filled in to imply an observer. */\nexport function parsePrintContrastReferenceInput(value: unknown): PrintContrastReferenceInput {\n const r = requireAllowlistedRecord(value, [\"referenceId\", \"stimulus\", \"observer\", \"meanLuminanceCdPerSquareMeter\", \"spatialFrequencyCyclesPerDegree\",\n \"gaussianEnvelopeSigmaDegrees\", \"temporalFrequencyHz\", \"eccentricityDegrees\", \"modulationMichelson\"], \"Invalid Print contrast reference fields.\");\n const number = (key: string, positive: boolean): number => {\n const v = r[key];\n if (typeof v !== \"number\" || !Number.isFinite(v) || (positive ? v <= 0 : v < 0)) throw new InvalidConfigurationError(\"Print contrast reference quantities must be finite and in their numeric domain.\");\n return v === 0 ? 0 : v;\n };\n if (r.stimulus !== \"static-neutral-d65-gabor\" || r.observer !== \"published-binocular-natural-pupil-reference\") throw new InvalidConfigurationError(\"Unknown Print reference stimulus/observer.\");\n const modulationMichelson = number(\"modulationMichelson\", false);\n if (modulationMichelson > 1) throw new InvalidConfigurationError(\"Reference Michelson modulation must be at most one.\");\n return { referenceId: requirePublicOpaqueId(r.referenceId, \"Invalid public Print contrast reference identity.\"), stimulus: r.stimulus, observer: r.observer,\n meanLuminanceCdPerSquareMeter: number(\"meanLuminanceCdPerSquareMeter\", true), spatialFrequencyCyclesPerDegree: number(\"spatialFrequencyCyclesPerDegree\", true),\n gaussianEnvelopeSigmaDegrees: number(\"gaussianEnvelopeSigmaDegrees\", true), temporalFrequencyHz: number(\"temporalFrequencyHz\", false),\n eccentricityDegrees: number(\"eccentricityDegrees\", false), modulationMichelson };\n}\n\n// Independently written scalar evaluation of the licensed numerical model.\n// Using log1p/expm1 keeps the small hyperbolic saturation term accurate.\nfunction luminanceDependency(p: readonly number[], luminance: number): number {\n if (p.length === 2) return p[1]! * luminance ** p[0]!;\n const first = p[0]! * Math.exp(-p[2]! * Math.log1p(p[1]! / luminance));\n return p.length === 3 ? first : first * -Math.expm1(-p[4]! * Math.log1p(p[3]! / luminance));\n}\nfunction component(frequency: number, area: number, peak: number, peakFrequency: number, bandwidth: number, lowFrequencyFloor: number, a0: number, f0: number): number {\n const logDifference = Math.log10(frequency) - Math.log10(peakFrequency);\n const parabola = 10 ** (-logDifference * logDifference / 2 ** bandwidth);\n const shape = frequency < peakFrequency ? Math.max(parabola, lowFrequencyFloor) : parabola;\n const criticalArea = a0 / (1 + (frequency / f0) ** 2);\n return peak * shape * Math.sqrt(criticalArea / (1 + criticalArea / area)) * frequency;\n}\nfunction staticNeutralSensitivity(luminance: number, frequency: number): number {\n const area = Math.PI * 1.5 ** 2;\n const sustained = component(frequency, area, luminanceDependency([56.4947, 7.54726, .144532, 5.58341e-7, 9.66862e9], luminance),\n luminanceDependency([1.78119, 91.5718, .256682], luminance), .000213047, 1 - .100207, 157.103, .702338);\n const transient = component(frequency, area, luminanceDependency([.193434, 2748.09], luminance), .000316696, 2.6761, 1 - .000241177, 3.81611, 3.01389);\n const omegaPeak = Math.log10(luminance) * 2.41482 + 4.7036;\n const achromatic = sustained + transient * Math.exp(-((omegaPeak ** .1898) ** 2) / .0844836);\n const redGreen = component(frequency, area, luminanceDependency([681.434, 38.0038, .480386], luminance), .0178364, 2.42104, 1, 2816.44, .0711058);\n const yellowViolet = component(frequency, area, luminanceDependency([166.683, 62.8974, .41193], luminance), .00425753, 2.68197, 1, 2.82789e7, .000635093);\n // Modulate each cone channel of the D65 LMS background [L,M,S] by the same fraction.\n // All three mechanism responses are kept, including nonzero colour-opponent parts.\n const redGreenProjection = Math.abs(.6991 - 2.3112 * .3009);\n const yellowVioletProjection = Math.abs(-.6991 - .3009 + 50.9875 * .0198);\n return Math.hypot(achromatic, redGreenProjection * redGreen, yellowVioletProjection * yellowViolet);\n}\n\n/** Estimates the detection threshold from the published model, within its limited range. It does not claim that detail is invisible to any particular person. */\nexport function calculatePrintContrastReference(value: PrintContrastReferenceInput): CalculationResult<PrintContrastReference> {\n const input = parsePrintContrastReferenceInput(value), l = input.meanLuminanceCdPerSquareMeter, f = input.spatialFrequencyCyclesPerDegree;\n const result: PrintContrastReference = { referenceId: input.referenceId, engineApiVersion: ENGINE_API_VERSION, modelVersion: PRINT_CONTRAST_REFERENCE_MODEL_VERSION,\n upstreamRevision: PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION, input, status: \"unsupported\", blockers: [], estimate: null,\n observerApplicability: \"population-reference-unverified-for-individual\", naturalImageVisibility: \"unassessed\", printStimulusMatch: \"unassessed\",\n uncertainty: \"not-quantified\", overallPrintVerdict: \"not-offered\" };\n if (l < 1 || l > 1000 || f < .25 || f > 16 || input.gaussianEnvelopeSigmaDegrees !== 1.5 || input.temporalFrequencyHz !== 0 || input.eccentricityDegrees !== 0) result.blockers.push(\"outside-static-neutral-reference-domain\");\n else {\n const sensitivity = staticNeutralSensitivity(l, f), threshold = 1 / sensitivity, ratio = input.modulationMichelson * sensitivity;\n if (![sensitivity, threshold, ratio].every(Number.isFinite) || sensitivity <= 0 || threshold <= 0) result.blockers.push(\"contrast-reference-numeric-range-unsupported\");\n else { result.status = \"model-estimate\"; result.estimate = { contrastSensitivity: sensitivity, thresholdModulationMichelson: threshold, suppliedModulationToModelThresholdRatio: ratio }; }\n }\n return freezeOwnedData(estimatedResult(result, \"castlecsf-static-neutral-d65-reference\", PRINT_CONTRAST_REFERENCE_MODEL_VERSION,\n [\"Static foveal D65 equal-fraction LMS Gabor; Gaussian envelope sigma is exactly 1.5 degrees.\",\n \"Published population model reference; personal eyesight, natural-image masking and actual print-stimulus matching remain unassessed.\",\n \"The numerical envelope is restricted; published model-fit errors do not become an individual confidence interval or a print verdict.\"], {\n validRanges: [\n { parameterPath: \"meanLuminanceCdPerSquareMeter\", unit: \"cd/m^2\", minimum: 1, maximum: 1000, note: \"Restricted numerical reference envelope; no individual calibration.\" },\n { parameterPath: \"spatialFrequencyCyclesPerDegree\", unit: \"cycles/degree\", minimum: .25, maximum: 16 },\n { parameterPath: \"gaussianEnvelopeSigmaDegrees\", unit: \"degree\", minimum: 1.5, maximum: 1.5 },\n { parameterPath: \"temporalFrequencyHz\", unit: \"Hz\", minimum: 0, maximum: 0 },\n { parameterPath: \"eccentricityDegrees\", unit: \"degree\", minimum: 0, maximum: 0 }\n ], notes: [\"Uncertainty is not quantified; numerical conformance is distinct from stimulus/observer qualification.\"]\n }));\n}\n"]}
|
|
1
|
+
{"version":3,"file":"print-contrast-reference.js","sourceRoot":"","sources":["../../src/output/print-contrast-reference.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n// castleCSF numerical model material: Copyright (c) 2023 Graphics and Displays\n// group - University of Cambridge. MIT terms retained in NOTICE (the full\n// licence text). Original bounded TypeScript adapter.\n\n/** Contrast detection threshold for a static, neutral (D65 white) Gabor patch, from the castleCSF\n * contrast sensitivity function (CSF) model. A Gabor patch is a sine grating under a Gaussian envelope.\n * It does not say whether detail in a photograph will be visible.\n * @see docs/PRINT_CONTRAST_REFERENCE.md for exact source, applicability and review gates.\n */\nimport { estimatedResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { freezeOwnedData } from \"../core/owned-data.js\";\nimport { requireAllowlistedRecord, requirePublicOpaqueId } from \"../core/record-validation.js\";\nimport { ENGINE_API_VERSION } from \"../core/version.js\";\n\nexport const PRINT_CONTRAST_REFERENCE_MODEL_VERSION = \"0.1.0\" as const;\n/** Identifies the exact MIT-licensed model source. It is not a local calibration. */\nexport const PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION = \"f4b0b722af83001d7af979281e06ca642d36e4e8\" as const;\n\n/** The narrow reference stimulus. It is separate from any measured photographic region of interest (ROI).\n * Positive values outside the model's range return \"unsupported\" instead of being extrapolated.\n */\nexport interface PrintContrastReferenceInput {\n referenceId: string;\n stimulus: \"static-neutral-d65-gabor\";\n observer: \"published-binocular-natural-pupil-reference\";\n /** Mean luminance of the uniform D65 background and grating. Not the average of a photographic ROI. */\n meanLuminanceCdPerSquareMeter: number;\n spatialFrequencyCyclesPerDegree: number;\n gaussianEnvelopeSigmaDegrees: number;\n temporalFrequencyHz: number;\n eccentricityDegrees: number;\n /** Fractional amplitude of the neutral grating before the Gaussian envelope. Not taken from ROI minimum and maximum values. */\n modulationMichelson: number;\n}\nexport interface PrintContrastReference {\n referenceId: string;\n engineApiVersion: string;\n modelVersion: typeof PRINT_CONTRAST_REFERENCE_MODEL_VERSION;\n upstreamRevision: typeof PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION;\n input: PrintContrastReferenceInput;\n status: \"model-estimate\" | \"unsupported\";\n blockers: string[];\n estimate: { contrastSensitivity: number; thresholdModulationMichelson: number; suppliedModulationToModelThresholdRatio: number } | null;\n observerApplicability: \"population-reference-unverified-for-individual\";\n naturalImageVisibility: \"unassessed\";\n printStimulusMatch: \"unassessed\";\n uncertainty: \"not-quantified\";\n overallPrintVerdict: \"not-offered\";\n}\n\n/** Strictly parses and copies the explicit reference conditions. No defaults are filled in to imply an observer. */\nexport function parsePrintContrastReferenceInput(value: unknown): PrintContrastReferenceInput {\n const r = requireAllowlistedRecord(value, [\"referenceId\", \"stimulus\", \"observer\", \"meanLuminanceCdPerSquareMeter\", \"spatialFrequencyCyclesPerDegree\",\n \"gaussianEnvelopeSigmaDegrees\", \"temporalFrequencyHz\", \"eccentricityDegrees\", \"modulationMichelson\"], \"Invalid Print contrast reference fields.\");\n const number = (key: string, positive: boolean): number => {\n const v = r[key];\n if (typeof v !== \"number\" || !Number.isFinite(v) || (positive ? v <= 0 : v < 0)) throw new InvalidConfigurationError(\"Print contrast reference quantities must be finite and in their numeric domain.\");\n return v === 0 ? 0 : v;\n };\n if (r.stimulus !== \"static-neutral-d65-gabor\" || r.observer !== \"published-binocular-natural-pupil-reference\") throw new InvalidConfigurationError(\"Unknown Print reference stimulus/observer.\");\n const modulationMichelson = number(\"modulationMichelson\", false);\n if (modulationMichelson > 1) throw new InvalidConfigurationError(\"Reference Michelson modulation must be at most one.\");\n return { referenceId: requirePublicOpaqueId(r.referenceId, \"Invalid public Print contrast reference identity.\"), stimulus: r.stimulus, observer: r.observer,\n meanLuminanceCdPerSquareMeter: number(\"meanLuminanceCdPerSquareMeter\", true), spatialFrequencyCyclesPerDegree: number(\"spatialFrequencyCyclesPerDegree\", true),\n gaussianEnvelopeSigmaDegrees: number(\"gaussianEnvelopeSigmaDegrees\", true), temporalFrequencyHz: number(\"temporalFrequencyHz\", false),\n eccentricityDegrees: number(\"eccentricityDegrees\", false), modulationMichelson };\n}\n\n// Independently written scalar evaluation of the licensed numerical model.\n// Using log1p/expm1 keeps the hyperbolic terms accurate where the small quantity k_s4/Y (about 5.6e-10 at 1000 cd/m²) would cancel.\nfunction luminanceDependency(p: readonly number[], luminance: number): number {\n if (p.length === 2) return p[1]! * luminance ** p[0]!;\n const first = p[0]! * Math.exp(-p[2]! * Math.log1p(p[1]! / luminance));\n return p.length === 3 ? first : first * -Math.expm1(-p[4]! * Math.log1p(p[3]! / luminance));\n}\nfunction component(frequency: number, area: number, peak: number, peakFrequency: number, bandwidth: number, lowFrequencyFloor: number, a0: number, f0: number): number {\n const logDifference = Math.log10(frequency) - Math.log10(peakFrequency);\n const parabola = 10 ** (-logDifference * logDifference / 2 ** bandwidth);\n const shape = frequency < peakFrequency ? Math.max(parabola, lowFrequencyFloor) : parabola;\n const criticalArea = a0 / (1 + (frequency / f0) ** 2);\n return peak * shape * Math.sqrt(criticalArea / (1 + criticalArea / area)) * frequency;\n}\nfunction staticNeutralSensitivity(luminance: number, frequency: number): number {\n const area = Math.PI * 1.5 ** 2;\n const sustained = component(frequency, area, luminanceDependency([56.4947, 7.54726, .144532, 5.58341e-7, 9.66862e9], luminance),\n luminanceDependency([1.78119, 91.5718, .256682], luminance), .000213047, 1 - .100207, 157.103, .702338);\n const transient = component(frequency, area, luminanceDependency([.193434, 2748.09], luminance), .000316696, 2.6761, 1 - .000241177, 3.81611, 3.01389);\n const omegaPeak = Math.log10(luminance) * 2.41482 + 4.7036;\n const achromatic = sustained + transient * Math.exp(-((omegaPeak ** .1898) ** 2) / .0844836);\n const redGreen = component(frequency, area, luminanceDependency([681.434, 38.0038, .480386], luminance), .0178364, 2.42104, 1, 2816.44, .0711058);\n const yellowViolet = component(frequency, area, luminanceDependency([166.683, 62.8974, .41193], luminance), .00425753, 2.68197, 1, 2.82789e7, .000635093);\n // Modulate each cone channel of the D65 LMS background [L,M,S] by the same fraction.\n // All three mechanism responses are kept, including nonzero colour-opponent parts.\n const redGreenProjection = Math.abs(.6991 - 2.3112 * .3009);\n const yellowVioletProjection = Math.abs(-.6991 - .3009 + 50.9875 * .0198);\n return Math.hypot(achromatic, redGreenProjection * redGreen, yellowVioletProjection * yellowViolet);\n}\n\n/** Estimates the detection threshold from the published model, within its limited range. It does not claim that detail is invisible to any particular person. */\nexport function calculatePrintContrastReference(value: PrintContrastReferenceInput): CalculationResult<PrintContrastReference> {\n const input = parsePrintContrastReferenceInput(value), l = input.meanLuminanceCdPerSquareMeter, f = input.spatialFrequencyCyclesPerDegree;\n const result: PrintContrastReference = { referenceId: input.referenceId, engineApiVersion: ENGINE_API_VERSION, modelVersion: PRINT_CONTRAST_REFERENCE_MODEL_VERSION,\n upstreamRevision: PRINT_CONTRAST_REFERENCE_UPSTREAM_REVISION, input, status: \"unsupported\", blockers: [], estimate: null,\n observerApplicability: \"population-reference-unverified-for-individual\", naturalImageVisibility: \"unassessed\", printStimulusMatch: \"unassessed\",\n uncertainty: \"not-quantified\", overallPrintVerdict: \"not-offered\" };\n if (l < 1 || l > 1000 || f < .25 || f > 16 || input.gaussianEnvelopeSigmaDegrees !== 1.5 || input.temporalFrequencyHz !== 0 || input.eccentricityDegrees !== 0) result.blockers.push(\"outside-static-neutral-reference-domain\");\n else {\n const sensitivity = staticNeutralSensitivity(l, f), threshold = 1 / sensitivity, ratio = input.modulationMichelson * sensitivity;\n if (![sensitivity, threshold, ratio].every(Number.isFinite) || sensitivity <= 0 || threshold <= 0) result.blockers.push(\"contrast-reference-numeric-range-unsupported\");\n else { result.status = \"model-estimate\"; result.estimate = { contrastSensitivity: sensitivity, thresholdModulationMichelson: threshold, suppliedModulationToModelThresholdRatio: ratio }; }\n }\n return freezeOwnedData(estimatedResult(result, \"castlecsf-static-neutral-d65-reference\", PRINT_CONTRAST_REFERENCE_MODEL_VERSION,\n [\"Static foveal D65 equal-fraction LMS Gabor; Gaussian envelope sigma is exactly 1.5 degrees.\",\n \"Published population model reference; personal eyesight, natural-image masking and actual print-stimulus matching remain unassessed.\",\n \"The numerical envelope is restricted; published model-fit errors do not become an individual confidence interval or a print verdict.\"], {\n validRanges: [\n { parameterPath: \"meanLuminanceCdPerSquareMeter\", unit: \"cd/m^2\", minimum: 1, maximum: 1000, note: \"Restricted numerical reference envelope; no individual calibration.\" },\n { parameterPath: \"spatialFrequencyCyclesPerDegree\", unit: \"cycles/degree\", minimum: .25, maximum: 16 },\n { parameterPath: \"gaussianEnvelopeSigmaDegrees\", unit: \"degree\", minimum: 1.5, maximum: 1.5 },\n { parameterPath: \"temporalFrequencyHz\", unit: \"Hz\", minimum: 0, maximum: 0 },\n { parameterPath: \"eccentricityDegrees\", unit: \"degree\", minimum: 0, maximum: 0 }\n ], notes: [\"Uncertainty is not quantified; numerical conformance is distinct from stimulus/observer qualification.\"]\n }));\n}\n"]}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/** Print size limit that counts the capture's blur and noise as well as its pixels (#574).
|
|
2
|
+
* A pure function: it makes no image and is not used by the capture path.
|
|
3
|
+
* @see docs/PRINT_PLANNING.md, "Print size limit with blur and noise", for the equations, sources and limits.
|
|
4
|
+
*/
|
|
5
|
+
import { type CalculationResult } from "../core/calculation-result.js";
|
|
6
|
+
import type { RasterDimensions } from "../sensor/sensor-geometry.js";
|
|
7
|
+
import type { PrintLength } from "./print-plan.js";
|
|
8
|
+
import type { SimulateCaptureSensorReport } from "../sensor/sensor-characteristics.js";
|
|
9
|
+
/** The capture's blur at the sensor plane, as blur-spot diameters in millimetres.
|
|
10
|
+
* `components` combines the parts by root-sum-square (an approximation, see the docs); `measured-total` is one measured diameter.
|
|
11
|
+
* A zero component is allowed and means that part is left out. Engine helpers give the parts: `calculateDefocusCircle` (defocus),
|
|
12
|
+
* `calculateAiryDisk` (diffraction, micrometres), `calculateProjectedMotionBlur` (motion length).
|
|
13
|
+
*/
|
|
14
|
+
export type PrintBlurInput = {
|
|
15
|
+
kind: "components";
|
|
16
|
+
lensMm?: number;
|
|
17
|
+
defocusMm?: number;
|
|
18
|
+
diffractionMm?: number;
|
|
19
|
+
motionMm?: number;
|
|
20
|
+
} | {
|
|
21
|
+
kind: "measured-total";
|
|
22
|
+
diameterMm: number;
|
|
23
|
+
};
|
|
24
|
+
/** The noise of one pixel of the printed raster, for the tone the caller cares about (for example mid-gray).
|
|
25
|
+
* `shot-and-read-noise` uses the engine's noise model (`calculateSignalToNoise`: Poisson shot noise plus RMS read noise);
|
|
26
|
+
* `pixel-snr` is a per-pixel signal-to-noise ratio the caller already has. `capture-noise-figure` (2.20.0) takes a capture's own
|
|
27
|
+
* mid-gray noise figure (#535: `result.sensor.midGrey` of a `simulateCapture` with `fast: false`), whose SNR S/σ_total counts photon
|
|
28
|
+
* shot, read, dark-current, quantization and any fixed-pattern noise; it is one sensor site's, so it applies as the pixel SNR when one
|
|
29
|
+
* printed pixel is one capture pixel. A clipped figure (the patch at white) is refused.
|
|
30
|
+
*/
|
|
31
|
+
export type PrintNoiseInput = {
|
|
32
|
+
kind: "shot-and-read-noise";
|
|
33
|
+
signalElectrons: number;
|
|
34
|
+
readNoiseElectrons: number;
|
|
35
|
+
} | {
|
|
36
|
+
kind: "pixel-snr";
|
|
37
|
+
snrLinear: number;
|
|
38
|
+
} | {
|
|
39
|
+
kind: "capture-noise-figure";
|
|
40
|
+
midGrey: NonNullable<SimulateCaptureSensorReport["midGrey"]>;
|
|
41
|
+
};
|
|
42
|
+
/** What "looks sharp and clean" means: the smallest angle the viewer resolves, and the lowest signal-to-noise ratio the viewer
|
|
43
|
+
* accepts after the eye averages over that angle. Both are the caller's choice; one arcminute is normal (20/20) acuity.
|
|
44
|
+
*/
|
|
45
|
+
export interface PrintSharpCleanCriterion {
|
|
46
|
+
kind: "visual-resolution";
|
|
47
|
+
arcminutes: number;
|
|
48
|
+
minimumVisibleSnr: number;
|
|
49
|
+
}
|
|
50
|
+
/** Input. The raster is the retained image that is printed (after any crop), with square pixels; `pixelPitchMm` is the
|
|
51
|
+
* sensor length one of those pixels covers. The print keeps the raster's aspect ratio. The viewing distance is fixed.
|
|
52
|
+
*/
|
|
53
|
+
export interface PrintSizeLimitWithBlurAndNoiseInput {
|
|
54
|
+
raster: RasterDimensions;
|
|
55
|
+
pixelPitchMm: number;
|
|
56
|
+
viewingDistance: PrintLength;
|
|
57
|
+
criterion: PrintSharpCleanCriterion;
|
|
58
|
+
blur: PrintBlurInput;
|
|
59
|
+
noise: PrintNoiseInput;
|
|
60
|
+
}
|
|
61
|
+
/** A print size in millimetres at the raster's aspect ratio. */
|
|
62
|
+
export interface PrintImageMm {
|
|
63
|
+
width: number;
|
|
64
|
+
height: number;
|
|
65
|
+
}
|
|
66
|
+
/** Result. Each factor's own limit is the largest print it allows on its own; null means that factor never limits.
|
|
67
|
+
* `limitingFactor` names the smallest; on a tie the order is pixels, blur, noise.
|
|
68
|
+
*/
|
|
69
|
+
export interface PrintSizeLimitWithBlurAndNoise {
|
|
70
|
+
engineApiVersion: string;
|
|
71
|
+
status: "finite" | "no-positive-size";
|
|
72
|
+
limitingFactor: "pixels" | "blur" | "noise";
|
|
73
|
+
maximumImageMm: PrintImageMm;
|
|
74
|
+
limits: {
|
|
75
|
+
pixels: PrintImageMm;
|
|
76
|
+
blur: PrintImageMm | null;
|
|
77
|
+
noise: PrintImageMm | null;
|
|
78
|
+
};
|
|
79
|
+
viewingDistanceMm: number;
|
|
80
|
+
/** The criterion angle as a length on the print at the viewing distance: 2 d tan(θ/2). */
|
|
81
|
+
resolvedSpotMm: number;
|
|
82
|
+
blur: {
|
|
83
|
+
diameterMm: number;
|
|
84
|
+
diameterPixels: number;
|
|
85
|
+
largestComponent: "lens" | "defocus" | "diffraction" | "motion" | "measured-total" | null;
|
|
86
|
+
};
|
|
87
|
+
noise: {
|
|
88
|
+
pixelSnr: number;
|
|
89
|
+
visibleSnrAtMaximum: number;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** Calculates the largest print, at a fixed viewing distance, at which the capture still looks sharp and clean, and names
|
|
93
|
+
* the factor that limits it: pixels, blur or noise. An approximation throughout; the equations are exact under its model.
|
|
94
|
+
*
|
|
95
|
+
* With θ the criterion angle, d the viewing distance, s = 2 d tan(θ/2) the length θ covers on the print, N_w the raster
|
|
96
|
+
* width in pixels, b the blur diameter in pixels and SNR_px the per-pixel SNR (print width W, height in proportion):
|
|
97
|
+
* - pixels: a print pixel (W / N_w) may cover at most s, so W ≤ N_w·s (as `calculatePrintPlan` with
|
|
98
|
+
* `angular-pixel-pitch`, before its whole-pixel rounding);
|
|
99
|
+
* - blur: the blur on the print (b·W / N_w) may cover at most s, so W ≤ N_w·s / b: the criterion of an acceptable circle
|
|
100
|
+
* of confusion with the eye's resolution as the limit;
|
|
101
|
+
* - noise: the eye averages the (W / N_w) wide pixels inside s, so the visible SNR is SNR_px·(N_w·s / W) while a pixel is
|
|
102
|
+
* smaller than s (the mean of n independent values has 1/√n of their standard deviation); W ≤ N_w·s·SNR_px / SNR_min,
|
|
103
|
+
* and noise never limits when SNR_px ≥ SNR_min.
|
|
104
|
+
* One arcminute is normal (20/20) acuity under optimal conditions (Z. Alomar, K. D. Bojikian, "Visual Acuity Testing in
|
|
105
|
+
* Adults", EyeWiki, American Academy of Ophthalmology, https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults, last edited
|
|
106
|
+
* June 16, 2026); the SNR threshold has no default and is the caller's. Blur parts add in root-sum-square, which is exact
|
|
107
|
+
* for Gaussian spots (variances of independent displacements add: C. M. Grinstead, J. L. Snell, "Introduction to
|
|
108
|
+
* Probability", 2nd rev. ed., AMS 1997, sec. 6.2, free under the GNU FDL) and an approximation for discs and Airy patterns.
|
|
109
|
+
*/
|
|
110
|
+
export declare function calculatePrintSizeLimitWithBlurAndNoise(value: PrintSizeLimitWithBlurAndNoiseInput): CalculationResult<PrintSizeLimitWithBlurAndNoise>;
|