@photivra/engine 2.16.0 → 2.18.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 +104 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +24 -0
- package/dist/camera/lens-projection.d.ts +5 -0
- package/dist/camera/lens-projection.js +10 -0
- package/dist/camera/lens-projection.js.map +1 -1
- package/dist/camera/sensor-equivalence.d.ts +156 -0
- package/dist/camera/sensor-equivalence.js +208 -0
- package/dist/camera/sensor-equivalence.js.map +1 -0
- package/dist/capture/native-environment-tabulated.d.ts +5 -0
- package/dist/capture/native-environment-tabulated.js +1 -1
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/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 +227 -0
- package/dist/equipment/generic-lens-catalogue.js +243 -0
- package/dist/equipment/generic-lens-catalogue.js.map +1 -0
- package/dist/equipment/ideal-lens.d.ts +25 -0
- package/dist/equipment/ideal-lens.js +21 -0
- package/dist/equipment/ideal-lens.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +94 -0
- package/dist/facade/catalogue-lens-signal.js +362 -0
- package/dist/facade/catalogue-lens-signal.js.map +1 -0
- package/dist/facade/defocus-bokeh-zones.d.ts +33 -0
- package/dist/facade/defocus-bokeh-zones.js +53 -0
- package/dist/facade/defocus-bokeh-zones.js.map +1 -0
- package/dist/facade/defocus-image-space-provider.d.ts +9 -0
- package/dist/facade/defocus-image-space-provider.js +101 -9
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +29 -0
- package/dist/facade/diffraction-provider.js +117 -11
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +4 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +10 -0
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.d.ts +61 -0
- package/dist/facade/display-view-lens-signal.js +502 -0
- package/dist/facade/display-view-lens-signal.js.map +1 -0
- package/dist/facade/expected-signal-setup.d.ts +15 -0
- package/dist/facade/expected-signal-setup.js +19 -2
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +13 -0
- package/dist/facade/expected-signal.js +155 -1
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +7 -0
- package/dist/facade/generic-lens-capture.js +9 -0
- package/dist/facade/generic-lens-capture.js.map +1 -0
- package/dist/facade/large-defocus-lattice.d.ts +39 -0
- package/dist/facade/large-defocus-lattice.js +64 -0
- package/dist/facade/large-defocus-lattice.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +5 -0
- package/dist/facade/scene-radiance.js +5 -2
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +67 -7
- package/dist/facade/simulate-capture.js +400 -64
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +23 -17
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +0 -9
- package/dist/gpu/display-view-quality-gpu.js +6 -0
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.js +2 -0
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-bokeh-kernel.d.ts +207 -0
- package/dist/optics/defocus-bokeh-kernel.js +380 -0
- package/dist/optics/defocus-bokeh-kernel.js.map +1 -0
- package/dist/optics/defocus-disc-kernel.d.ts +41 -19
- package/dist/optics/defocus-disc-kernel.js +44 -23
- package/dist/optics/defocus-disc-kernel.js.map +1 -1
- package/dist/optics/generic-lens-psf.d.ts +68 -0
- package/dist/optics/generic-lens-psf.js +337 -0
- package/dist/optics/generic-lens-psf.js.map +1 -0
- package/dist/optics/illumination-vignetting.d.ts +6 -0
- package/dist/optics/illumination-vignetting.js +24 -7
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/sensor-environment-query.d.ts +6 -0
- package/dist/optics/sensor-environment-query.js +13 -6
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-kernel-runs.d.ts +77 -0
- package/dist/optics/site-kernel-runs.js +200 -0
- package/dist/optics/site-kernel-runs.js.map +1 -0
- package/dist/output/generic-lens-corrections.d.ts +24 -0
- package/dist/output/generic-lens-corrections.js +51 -0
- package/dist/output/generic-lens-corrections.js.map +1 -0
- package/docs/AGENT_GUIDE.md +4 -0
- package/docs/API_REFERENCE.md +77 -1
- package/docs/QUICK_START.md +305 -17
- package/docs/USAGE.md +98 -5
- package/docs/api/camera-sensor-equivalence.md +209 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +281 -0
- package/docs/api/equipment-ideal-lens.md +26 -0
- package/docs/api/exports.json +202 -2
- package/docs/api/facade-simulate-capture.md +48 -4
- package/docs/api/optics-defocus-bokeh-kernel.md +145 -0
- package/docs/api/optics-defocus-disc-kernel.md +22 -15
- package/docs/api/optics-sensor-environment-query.md +6 -0
- package/docs/api/output-generic-lens-corrections.md +39 -0
- package/llms.txt +3 -0
- package/package.json +1 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import { approximationResult } from "../core/calculation-result.js";
|
|
22
22
|
import { InvalidScientificInputError } from "../core/validation.js";
|
|
23
23
|
import { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping } from "../motion/camera-rotation.js";
|
|
24
|
-
import { lensProjectionDirectionValue, parseLensProjectionModel } from "../camera/lens-projection.js";
|
|
24
|
+
import { insideRadialImageCircle, lensProjectionDirectionValue, parseLensProjectionModel } from "../camera/lens-projection.js";
|
|
25
25
|
import { calculateInverseRadialSourcePointValue, validateRadialDistortionProfile } from "./radial-distortion.js";
|
|
26
26
|
import { brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile } from "./brown-conrady-distortion.js";
|
|
27
27
|
import { calculateThinLensImageDistance } from "./thin-lens.js";
|
|
@@ -37,7 +37,7 @@ export function prepareEnvironmentLensFieldMapping(value) {
|
|
|
37
37
|
}
|
|
38
38
|
const record = value;
|
|
39
39
|
for (const key of Object.keys(record)) {
|
|
40
|
-
if (!["projectionModel", "distortion", "radiometricAreaCorrection"].includes(key)) {
|
|
40
|
+
if (!["projectionModel", "distortion", "radiometricAreaCorrection", "outsideImageCircle"].includes(key)) {
|
|
41
41
|
throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
@@ -45,22 +45,26 @@ export function prepareEnvironmentLensFieldMapping(value) {
|
|
|
45
45
|
throw new InvalidConfigurationError("lensFieldMapping.radiometricAreaCorrection must be \"inverse-distortion-jacobian\".");
|
|
46
46
|
}
|
|
47
47
|
const projectionModel = parseLensProjectionModel(record.projectionModel);
|
|
48
|
+
if (record.outsideImageCircle !== undefined && record.outsideImageCircle !== "backward-ray") {
|
|
49
|
+
throw new InvalidConfigurationError("lensFieldMapping.outsideImageCircle must be \"backward-ray\" when given.");
|
|
50
|
+
}
|
|
51
|
+
const outside = record.outsideImageCircle === undefined ? {} : { outsideImageCircle: "backward-ray" };
|
|
48
52
|
const distortion = record.distortion;
|
|
49
53
|
if (typeof distortion !== "object" || distortion === null) {
|
|
50
54
|
throw new InvalidConfigurationError("lensFieldMapping.distortion must be an object.");
|
|
51
55
|
}
|
|
52
56
|
if (distortion.kind === "none") {
|
|
53
|
-
return { mapping: { projectionModel, distortion: { kind: "none" }, radiometricAreaCorrection: "inverse-distortion-jacobian" } };
|
|
57
|
+
return { mapping: { projectionModel, distortion: { kind: "none" }, radiometricAreaCorrection: "inverse-distortion-jacobian", ...outside } };
|
|
54
58
|
}
|
|
55
59
|
if (distortion.kind === "radial") {
|
|
56
60
|
const radial = validateRadialDistortionProfile(distortion.profile);
|
|
57
61
|
return { mapping: { projectionModel, distortion: { kind: "radial", profile: distortion.profile },
|
|
58
|
-
radiometricAreaCorrection: "inverse-distortion-jacobian" }, radial };
|
|
62
|
+
radiometricAreaCorrection: "inverse-distortion-jacobian", ...outside }, radial };
|
|
59
63
|
}
|
|
60
64
|
if (distortion.kind === "brown-conrady") {
|
|
61
65
|
const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);
|
|
62
66
|
return { mapping: { projectionModel, distortion: { kind: "brown-conrady", profile: distortion.profile },
|
|
63
|
-
radiometricAreaCorrection: "inverse-distortion-jacobian" }, brownConrady };
|
|
67
|
+
radiometricAreaCorrection: "inverse-distortion-jacobian", ...outside }, brownConrady };
|
|
64
68
|
}
|
|
65
69
|
throw new InvalidConfigurationError("lensFieldMapping.distortion.kind must be none, radial or brown-conrady.");
|
|
66
70
|
}
|
|
@@ -190,7 +194,10 @@ export function calculateMappedSensorEnvironmentRadianceQuery(input, prepared) {
|
|
|
190
194
|
if (typeof t !== "number" || !Number.isFinite(t) || t < 0) {
|
|
191
195
|
throw new InvalidScientificInputError("timeSecondsFromOpeningReference must be finite and non-negative.");
|
|
192
196
|
}
|
|
193
|
-
|
|
197
|
+
// #549: outside the image circle, with "backward-ray", look straight back (no forward scene there).
|
|
198
|
+
const captured = mapping.outsideImageCircle === "backward-ray" &&
|
|
199
|
+
!insideRadialImageCircle(mapping.projectionModel, projectionScaleMm, Math.hypot(ideal.x, ideal.y)) ? { x: 0, y: 0, z: -1 } :
|
|
200
|
+
lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);
|
|
194
201
|
const reference = applyPhysicalCameraRotation(captured, { pitch: velocity.pitch * t, yaw: velocity.yaw * t, roll: velocity.roll * t }, "camera-to-reference");
|
|
195
202
|
const length = hypot3(reference.x, reference.y, reference.z);
|
|
196
203
|
if (!(length > 0) || !Number.isFinite(length))
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal The ideal (undistorted) image point of a physical sensor point (+X right, +Y up, mm) and the distortion area\n * factor 1 / det(d distorted / d ideal) there; the ideal point is the sensor point itself without distortion. Shared by\n * the mapped query below and the image-space defocus of `simulateCapture` (#548), so both use one arithmetic.\n */\nexport function undistortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, imagePointMm: { x: number; y: number }):\n { ideal: { x: number; y: number }; distortionAreaFactor: number } {\n let ideal = imagePointMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(imagePointMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, imagePointMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n return { ideal, distortionAreaFactor };\n}\n\n/**\n * @internal The forward distortion of an ideal image point (+X right, +Y up, mm) to the physical sensor point, with the\n * singular values of d distorted / d ideal there (the largest and smallest local scale of the map; 1 and 1 without\n * distortion). The inverse of {@link undistortedImagePoint} up to its solver's rounding (#548).\n */\nexport function distortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, ideal: { x: number; y: number }):\n { pointMm: { x: number; y: number }; largestScale: number; smallestScale: number } {\n if (prepared.radial !== undefined) {\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n // Tangential scale s, radial scale s + r ds/dr (the two eigenvalues of the symmetric Jacobian of p -> s(r) p).\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n return { pointMm: { x: ideal.x*scale, y: ideal.y*scale }, largestScale: Math.max(scale, radialDerivative), smallestScale: Math.min(scale, radialDerivative) };\n }\n if (prepared.brownConrady !== undefined) {\n const forward = brownConradyForwardValue(prepared.brownConrady, ideal), [a, b, c, d] = forward.jacobian;\n // Singular values of [[a, b], [c, d]]: sqrt of the eigenvalues of J^T J.\n const p = a*a+b*b+c*c+d*d, q = Math.abs(a*d-b*c), root = Math.sqrt(Math.max(0, p*p-4*q*q));\n const largest = Math.sqrt((p+root)/2);\n return { pointMm: forward.mappedImagePointMm, largestScale: largest, smallestScale: largest > 0 ? q/largest : 0 };\n }\n return { pointMm: { ...ideal }, largestScale: 1, smallestScale: 1 };\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n const { ideal, distortionAreaFactor } = undistortedImagePoint(prepared, sourcePointImagePlaneMm);\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = hypot3(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { insideRadialImageCircle, lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n /**\n * #549, optional: what an ideal image point outside a radial projection's image circle (for example beyond r = 2f of the\n * equisolid fisheye) does. Omitted: the query fails, as before. `\"backward-ray\"`: the query looks straight back (0, 0, −1),\n * a direction no forward scene answers, so a provider that treats directions beyond 90° as empty gives it zero radiance.\n */\n outsideImageCircle?: \"backward-ray\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\", \"outsideImageCircle\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n if (record.outsideImageCircle !== undefined && record.outsideImageCircle !== \"backward-ray\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.outsideImageCircle must be \\\"backward-ray\\\" when given.\");\n }\n const outside = record.outsideImageCircle === undefined ? {} : { outsideImageCircle: \"backward-ray\" as const };\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\", ...outside } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\", ...outside }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\", ...outside }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal The ideal (undistorted) image point of a physical sensor point (+X right, +Y up, mm) and the distortion area\n * factor 1 / det(d distorted / d ideal) there; the ideal point is the sensor point itself without distortion. Shared by\n * the mapped query below and the image-space defocus of `simulateCapture` (#548), so both use one arithmetic.\n */\nexport function undistortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, imagePointMm: { x: number; y: number }):\n { ideal: { x: number; y: number }; distortionAreaFactor: number } {\n let ideal = imagePointMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(imagePointMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, imagePointMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n return { ideal, distortionAreaFactor };\n}\n\n/**\n * @internal The forward distortion of an ideal image point (+X right, +Y up, mm) to the physical sensor point, with the\n * singular values of d distorted / d ideal there (the largest and smallest local scale of the map; 1 and 1 without\n * distortion). The inverse of {@link undistortedImagePoint} up to its solver's rounding (#548).\n */\nexport function distortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, ideal: { x: number; y: number }):\n { pointMm: { x: number; y: number }; largestScale: number; smallestScale: number } {\n if (prepared.radial !== undefined) {\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n // Tangential scale s, radial scale s + r ds/dr (the two eigenvalues of the symmetric Jacobian of p -> s(r) p).\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n return { pointMm: { x: ideal.x*scale, y: ideal.y*scale }, largestScale: Math.max(scale, radialDerivative), smallestScale: Math.min(scale, radialDerivative) };\n }\n if (prepared.brownConrady !== undefined) {\n const forward = brownConradyForwardValue(prepared.brownConrady, ideal), [a, b, c, d] = forward.jacobian;\n // Singular values of [[a, b], [c, d]]: sqrt of the eigenvalues of J^T J.\n const p = a*a+b*b+c*c+d*d, q = Math.abs(a*d-b*c), root = Math.sqrt(Math.max(0, p*p-4*q*q));\n const largest = Math.sqrt((p+root)/2);\n return { pointMm: forward.mappedImagePointMm, largestScale: largest, smallestScale: largest > 0 ? q/largest : 0 };\n }\n return { pointMm: { ...ideal }, largestScale: 1, smallestScale: 1 };\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n const { ideal, distortionAreaFactor } = undistortedImagePoint(prepared, sourcePointImagePlaneMm);\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n // #549: outside the image circle, with \"backward-ray\", look straight back (no forward scene there).\n const captured = mapping.outsideImageCircle === \"backward-ray\" &&\n !insideRadialImageCircle(mapping.projectionModel, projectionScaleMm, Math.hypot(ideal.x, ideal.y)) ? { x: 0, y: 0, z: -1 } :\n lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = hypot3(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Largest kernel radius convolved directly ((2R + 1)² taps per output, the method of 2.16.0 and earlier). Requests within it
|
|
3
|
+
* render byte-identically to 2.16.0; above it, the run convolution of this module applies.
|
|
4
|
+
*/
|
|
5
|
+
export declare const DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES = 64;
|
|
6
|
+
/**
|
|
7
|
+
* Most reads per output the run convolution accepts: the tap count of the direct gather at radius 64, (2·64 + 1)², so a large
|
|
8
|
+
* kernel never costs more per output than the largest direct one. The disc stays far below it (6,145 at radius 512).
|
|
9
|
+
*/
|
|
10
|
+
export declare const SITE_KERNEL_RUNS_MAXIMUM_READS: number;
|
|
11
|
+
/** A site kernel as weighted runs (prefix differences) and single taps, in a fixed order. */
|
|
12
|
+
export interface SiteKernelRuns {
|
|
13
|
+
/** Kernel radius R: the window is (2R + 1)². */
|
|
14
|
+
readonly radius: number;
|
|
15
|
+
/** Distinct run weights, in order of first appearance (row-major). */
|
|
16
|
+
readonly groupWeights: Float64Array;
|
|
17
|
+
/** Runs of group g are runStart[g] … runStart[g + 1] − 1. */
|
|
18
|
+
readonly runStart: Int32Array;
|
|
19
|
+
/** Each run's kernel row v and columns u0 ≤ u1 (offsets from the centre). */
|
|
20
|
+
readonly runV: Int32Array;
|
|
21
|
+
readonly runU0: Int32Array;
|
|
22
|
+
readonly runU1: Int32Array;
|
|
23
|
+
/** Single taps: row v, column u, weight. */
|
|
24
|
+
readonly tapV: Int32Array;
|
|
25
|
+
readonly tapU: Int32Array;
|
|
26
|
+
readonly tapW: Float64Array;
|
|
27
|
+
/** Reads per output: 2 per run plus 1 per tap. */
|
|
28
|
+
readonly readsPerOutput: number;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The run decomposition of a site kernel of radius R ((2R + 1)² row-major weights, out(p) = Σ_t K(t)·L(p − t)). Zero taps are
|
|
32
|
+
* skipped. Fails with `InvalidConfigurationError` when the kernel would need more than {@link SITE_KERNEL_RUNS_MAXIMUM_READS}
|
|
33
|
+
* reads per output, with a short `lens.defocus:` message (the kernels it takes are `lens.defocus`'s; spherical aberration's ray binning
|
|
34
|
+
* is the one that can reach the limit).
|
|
35
|
+
*/
|
|
36
|
+
export declare function siteKernelRuns(weights: Float64Array, radius: number): SiteKernelRuns;
|
|
37
|
+
/** Which outputs of a tile row to compute: columns start, start + step, … (tile coordinates); null skips the row. */
|
|
38
|
+
export type SiteKernelRunsRowPattern = (tileRow: number) => {
|
|
39
|
+
start: number;
|
|
40
|
+
step: number;
|
|
41
|
+
} | null;
|
|
42
|
+
/**
|
|
43
|
+
* Convolves a source rectangle with the runs: out(x, y) = Σ_t K(t)·source(x + R − t_u, y + R − t_v) for 0 ≤ x < width,
|
|
44
|
+
* 0 ≤ y < height, the source holding (width + 2R) × (height + 2R) samples row-major. Each output is the sum over the weight
|
|
45
|
+
* groups, in order, of the group weight times its runs' prefix differences (runs in order), then the single taps in order.
|
|
46
|
+
* Outputs a `pattern` skips are 0. Samples are checked as `calculateIrradianceConvolution` checks them.
|
|
47
|
+
*/
|
|
48
|
+
export declare function convolveSiteKernelRuns(source: Float64Array, width: number, height: number, runs: SiteKernelRuns, pattern?: SiteKernelRunsRowPattern): Float64Array;
|
|
49
|
+
/**
|
|
50
|
+
* The source of one tile: called with the source rectangle's origin and size (lattice coordinates; the tile plus the radius on
|
|
51
|
+
* every side), it returns a function giving each field's samples, row-major, called once per field in order (so a caller can
|
|
52
|
+
* share work between fields, such as one material lookup per point, and keep only one field's samples at a time).
|
|
53
|
+
*/
|
|
54
|
+
export type SiteKernelRunsTileSource = (x0: number, y0: number, width: number, height: number) => (field: number) => Float64Array;
|
|
55
|
+
export interface SiteKernelRunsLatticeInput {
|
|
56
|
+
runs: SiteKernelRuns;
|
|
57
|
+
/** Tile side T in lattice points (even, so CFA parity patterns are the same in every tile). */
|
|
58
|
+
tile: number;
|
|
59
|
+
fields: number;
|
|
60
|
+
source: SiteKernelRunsTileSource;
|
|
61
|
+
/** Per field, the outputs of each tile row to compute (absolute row parity is the tile row's, T being even). Default all. */
|
|
62
|
+
pattern?: (field: number) => SiteKernelRunsRowPattern | undefined;
|
|
63
|
+
/** Bytes of output tiles kept (least recently used dropped first). Default 64 MiB. */
|
|
64
|
+
capacityBytes?: number;
|
|
65
|
+
/** Called with the duration in ms of each tile (diagnostics only). */
|
|
66
|
+
onTile?: (milliseconds: number) => void;
|
|
67
|
+
}
|
|
68
|
+
/** Convolved fields on a lattice, by fixed tiles (see the module comment), computed on first use and kept within a byte budget. */
|
|
69
|
+
export interface SiteKernelRunsLattice {
|
|
70
|
+
/** Field `field` of the convolved lattice at lattice point (x, y) (any integers). */
|
|
71
|
+
valueAt(field: number, x: number, y: number): number;
|
|
72
|
+
/** The fields of tile (tx, ty): T × T values each, row-major, for lattice points [tx·T, (tx + 1)·T) × [ty·T, (ty + 1)·T). Read only. */
|
|
73
|
+
tileAt(tx: number, ty: number): readonly Float64Array[];
|
|
74
|
+
/** Tiles computed so far (diagnostics). */
|
|
75
|
+
readonly tilesComputed: number;
|
|
76
|
+
}
|
|
77
|
+
export declare function createSiteKernelRunsLattice(input: SiteKernelRunsLatticeInput): SiteKernelRunsLattice;
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Large defocus kernels (#564): an exact convolution with a site kernel whose cost per output grows with the kernel's
|
|
4
|
+
* perimeter, not its area, for kernels of radius above {@link DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES}.
|
|
5
|
+
*
|
|
6
|
+
* Method. Each kernel row is split into runs of consecutive taps with the same weight (run-length encoding). A run's
|
|
7
|
+
* contribution to the output, w·Σ_{u = u0}^{u1} L(x − u, y − v), is the difference of two prefix sums of the source row
|
|
8
|
+
* y − v, so it costs two reads however long the run is. Runs with the same weight are summed first and multiplied once.
|
|
9
|
+
* This is the summed-area (integral image) evaluation of a box filter applied row by row: Crow, "Summed-area tables for
|
|
10
|
+
* texture mapping", SIGGRAPH 1984 (ACM, free to read), and Heckbert, "Filtering by repeated integration", SIGGRAPH 1986
|
|
11
|
+
* (free), which convolves any piecewise-constant kernel this way.
|
|
12
|
+
*
|
|
13
|
+
* The geometric defocus disc (`createDefocusDiscSiteKernel`) has one weight for every cell wholly inside the disc (the
|
|
14
|
+
* cell's area is exactly 1, so its weight is 1 divided by the total, the same double for all of them); only the cells the
|
|
15
|
+
* circle crosses, and the centre (which takes up the normalization's rounding), have their own weights. So the disc of
|
|
16
|
+
* radius R costs one run per row (2R + 1 of them) plus about 8R single taps, against (2R + 1)² taps of the direct gather:
|
|
17
|
+
* about 1,800 reads per output at R = 179 against 128,881. The shaped kernels of #553 without spherical aberration (blade
|
|
18
|
+
* polygons, the cat's eye) have the same structure (a uniform interior, exact polygon areas at the edge); a kernel with
|
|
19
|
+
* no equal neighbouring weights (spherical aberration's ray binning) has no runs and costs as much as the direct gather,
|
|
20
|
+
* so {@link siteKernelRuns} refuses one whose reads per output exceed {@link SITE_KERNEL_RUNS_MAXIMUM_READS}.
|
|
21
|
+
*
|
|
22
|
+
* It is not an approximation: every output is the same sum of the same products as the direct gather, regrouped. The
|
|
23
|
+
* regrouping changes only the rounding. The prefix sums of a row are accumulated from the left edge of the source tile,
|
|
24
|
+
* so a run's difference carries an absolute rounding error of a few ulps of the row's prefix (at most the sum over the tile's
|
|
25
|
+
* source row, (T + 2R) samples), and an output sums 2R + 1 run sums instead of (2R + 1)² products. Measured on the Siemens
|
|
26
|
+
* star, slanted edge and dead leaves at radii 65, 179 and 512 against a compensated (Neumaier) sum of the same products: within
|
|
27
|
+
* 2.5e-14 relative, where the direct gather itself was up to 1e-11 off (scripts/benchmark-large-defocus.mjs accuracy). Prefix
|
|
28
|
+
* sums of non-negative samples never decrease, so every run sum, and every output, is non-negative.
|
|
29
|
+
*
|
|
30
|
+
* Window independence. Outputs are computed in fixed square tiles of T × T lattice points at absolute positions
|
|
31
|
+
* (tile (tx, ty) holds [tx·T, (tx + 1)·T) × [ty·T, (ty + 1)·T)), each from its own source rectangle, the tile plus R on
|
|
32
|
+
* every side. A value therefore depends only on its lattice point and the request, never on the window, region or order
|
|
33
|
+
* of the queries: regions and parts are bit-identical to the whole frame.
|
|
34
|
+
*/
|
|
35
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
36
|
+
/**
|
|
37
|
+
* Largest kernel radius convolved directly ((2R + 1)² taps per output, the method of 2.16.0 and earlier). Requests within it
|
|
38
|
+
* render byte-identically to 2.16.0; above it, the run convolution of this module applies.
|
|
39
|
+
*/
|
|
40
|
+
export const DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES = 64;
|
|
41
|
+
/**
|
|
42
|
+
* Most reads per output the run convolution accepts: the tap count of the direct gather at radius 64, (2·64 + 1)², so a large
|
|
43
|
+
* kernel never costs more per output than the largest direct one. The disc stays far below it (6,145 at radius 512).
|
|
44
|
+
*/
|
|
45
|
+
export const SITE_KERNEL_RUNS_MAXIMUM_READS = 129 * 129;
|
|
46
|
+
/** Shortest run summed by prefix differences; shorter runs are read tap by tap (fewer reads). */
|
|
47
|
+
const MINIMUM_RUN = 3;
|
|
48
|
+
const MAXIMUM_SAMPLE = 2 ** 100;
|
|
49
|
+
/**
|
|
50
|
+
* The run decomposition of a site kernel of radius R ((2R + 1)² row-major weights, out(p) = Σ_t K(t)·L(p − t)). Zero taps are
|
|
51
|
+
* skipped. Fails with `InvalidConfigurationError` when the kernel would need more than {@link SITE_KERNEL_RUNS_MAXIMUM_READS}
|
|
52
|
+
* reads per output, with a short `lens.defocus:` message (the kernels it takes are `lens.defocus`'s; spherical aberration's ray binning
|
|
53
|
+
* is the one that can reach the limit).
|
|
54
|
+
*/
|
|
55
|
+
export function siteKernelRuns(weights, radius) {
|
|
56
|
+
const width = 2 * radius + 1;
|
|
57
|
+
if (!Number.isInteger(radius) || radius < 0 || weights.length !== width * width)
|
|
58
|
+
throw new InvalidConfigurationError("Site kernels must be (2R + 1)² taps.");
|
|
59
|
+
const groups = new Map(), tapV = [], tapU = [], tapW = [];
|
|
60
|
+
for (let v = -radius; v <= radius; v++) {
|
|
61
|
+
const row = (v + radius) * width;
|
|
62
|
+
for (let u = -radius; u <= radius;) {
|
|
63
|
+
const w = weights[row + u + radius];
|
|
64
|
+
let end = u;
|
|
65
|
+
while (end + 1 <= radius && weights[row + end + 1 + radius] === w)
|
|
66
|
+
end++;
|
|
67
|
+
if (w !== 0) {
|
|
68
|
+
if (end - u + 1 >= MINIMUM_RUN) {
|
|
69
|
+
let list = groups.get(w);
|
|
70
|
+
if (!list) {
|
|
71
|
+
list = [];
|
|
72
|
+
groups.set(w, list);
|
|
73
|
+
}
|
|
74
|
+
list.push(v, u, end);
|
|
75
|
+
}
|
|
76
|
+
else
|
|
77
|
+
for (let k = u; k <= end; k++) {
|
|
78
|
+
tapV.push(v);
|
|
79
|
+
tapU.push(k);
|
|
80
|
+
tapW.push(w);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
u = end + 1;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const groupWeights = Float64Array.from(groups.keys()), runStart = new Int32Array(groupWeights.length + 1), runV = [], runU0 = [], runU1 = [];
|
|
87
|
+
let g = 0;
|
|
88
|
+
for (const list of groups.values()) {
|
|
89
|
+
for (let k = 0; k < list.length; k += 3) {
|
|
90
|
+
runV.push(list[k]);
|
|
91
|
+
runU0.push(list[k + 1]);
|
|
92
|
+
runU1.push(list[k + 2]);
|
|
93
|
+
}
|
|
94
|
+
runStart[++g] = runV.length;
|
|
95
|
+
}
|
|
96
|
+
const readsPerOutput = 2 * runV.length + tapW.length;
|
|
97
|
+
if (readsPerOutput > SITE_KERNEL_RUNS_MAXIMUM_READS)
|
|
98
|
+
throw new InvalidConfigurationError("lens.defocus: this blur needs " + grouped(readsPerOutput) + " reads per site; above a kernel radius of " +
|
|
99
|
+
DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES + " sites the limit is " + grouped(SITE_KERNEL_RUNS_MAXIMUM_READS) + ". Reduce the spherical aberration or the defocus.");
|
|
100
|
+
return { radius, groupWeights, runStart, runV: Int32Array.from(runV), runU0: Int32Array.from(runU0), runU1: Int32Array.from(runU1),
|
|
101
|
+
tapV: Int32Array.from(tapV), tapU: Int32Array.from(tapU), tapW: Float64Array.from(tapW), readsPerOutput };
|
|
102
|
+
}
|
|
103
|
+
/** A non-negative integer with comma thousands separators (no locale data). */
|
|
104
|
+
const grouped = (value) => String(value).replace(/\B(?=(\d{3})+$)/gu, ",");
|
|
105
|
+
/**
|
|
106
|
+
* Convolves a source rectangle with the runs: out(x, y) = Σ_t K(t)·source(x + R − t_u, y + R − t_v) for 0 ≤ x < width,
|
|
107
|
+
* 0 ≤ y < height, the source holding (width + 2R) × (height + 2R) samples row-major. Each output is the sum over the weight
|
|
108
|
+
* groups, in order, of the group weight times its runs' prefix differences (runs in order), then the single taps in order.
|
|
109
|
+
* Outputs a `pattern` skips are 0. Samples are checked as `calculateIrradianceConvolution` checks them.
|
|
110
|
+
*/
|
|
111
|
+
export function convolveSiteKernelRuns(source, width, height, runs, pattern) {
|
|
112
|
+
const R = runs.radius, sw = width + 2 * R, sh = height + 2 * R, pw = sw + 1;
|
|
113
|
+
if (source.length !== sw * sh)
|
|
114
|
+
throw new InvalidConfigurationError("The run convolution's source must hold the tile and the kernel radius on every side.");
|
|
115
|
+
// Row prefix sums from the source tile's left edge: P(r, k) = Σ_{c < k} source(c, r).
|
|
116
|
+
const prefix = new Float64Array(pw * sh);
|
|
117
|
+
for (let r = 0; r < sh; r++) {
|
|
118
|
+
let sum = 0;
|
|
119
|
+
const s = r * sw, p = r * pw;
|
|
120
|
+
for (let c = 0; c < sw; c++) {
|
|
121
|
+
const value = source[s + c];
|
|
122
|
+
if (!(value >= 0 && value <= MAXIMUM_SAMPLE))
|
|
123
|
+
throw new InvalidConfigurationError("Irradiance samples must be finite, non-negative and within the declared magnitude.");
|
|
124
|
+
sum += value;
|
|
125
|
+
prefix[p + c + 1] = sum;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const out = new Float64Array(width * height), group = new Float64Array(width);
|
|
129
|
+
const { groupWeights, runStart, runV, runU0, runU1, tapV, tapU, tapW } = runs;
|
|
130
|
+
for (let y = 0; y < height; y++) {
|
|
131
|
+
const row = pattern ? pattern(y) : { start: 0, step: 1 };
|
|
132
|
+
if (!row)
|
|
133
|
+
continue;
|
|
134
|
+
const { start, step } = row, o = y * width;
|
|
135
|
+
for (let g = 0; g < groupWeights.length; g++) {
|
|
136
|
+
group.fill(0);
|
|
137
|
+
for (let k = runStart[g]; k < runStart[g + 1]; k++) {
|
|
138
|
+
// Σ_{u = u0}^{u1} source(x + R − u, y + R − v) = P(y + R − v, x + R − u0 + 1) − P(y + R − v, x + R − u1).
|
|
139
|
+
const p = (y + R - runV[k]) * pw, hi = p + R - runU0[k] + 1, lo = p + R - runU1[k];
|
|
140
|
+
for (let x = start; x < width; x += step)
|
|
141
|
+
group[x] = group[x] + (prefix[hi + x] - prefix[lo + x]);
|
|
142
|
+
}
|
|
143
|
+
const w = groupWeights[g];
|
|
144
|
+
for (let x = start; x < width; x += step)
|
|
145
|
+
out[o + x] = out[o + x] + w * group[x];
|
|
146
|
+
}
|
|
147
|
+
for (let k = 0; k < tapW.length; k++) {
|
|
148
|
+
const s = (y + R - tapV[k]) * sw + R - tapU[k], w = tapW[k];
|
|
149
|
+
for (let x = start; x < width; x += step)
|
|
150
|
+
out[o + x] = out[o + x] + w * source[s + x];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return out;
|
|
154
|
+
}
|
|
155
|
+
/** Tile key: tiles within ±2^20 of the origin in each direction. */
|
|
156
|
+
const tileKey = (tx, ty) => (ty + 1048576) * 2097152 + tx + 1048576;
|
|
157
|
+
export function createSiteKernelRunsLattice(input) {
|
|
158
|
+
const { runs, tile: T, fields } = input, R = runs.radius;
|
|
159
|
+
if (!Number.isInteger(T) || T < 2 || T % 2 !== 0)
|
|
160
|
+
throw new InvalidConfigurationError("The run lattice's tile side must be an even integer of 2 or more.");
|
|
161
|
+
const capacity = Math.max(1, Math.floor((input.capacityBytes ?? 64 * 1048576) / (fields * T * T * 8)));
|
|
162
|
+
const cache = new Map();
|
|
163
|
+
const now = () => globalThis.performance?.now() ?? Date.now();
|
|
164
|
+
let computed = 0, lastKey = NaN, last;
|
|
165
|
+
function tileOf(tx, ty) {
|
|
166
|
+
const key = tileKey(tx, ty);
|
|
167
|
+
if (key === lastKey)
|
|
168
|
+
return last;
|
|
169
|
+
let values = cache.get(key);
|
|
170
|
+
if (values) {
|
|
171
|
+
cache.delete(key);
|
|
172
|
+
cache.set(key, values);
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const started = input.onTile ? now() : 0;
|
|
176
|
+
const fieldSource = input.source(tx * T - R, ty * T - R, T + 2 * R, T + 2 * R);
|
|
177
|
+
values = [];
|
|
178
|
+
for (let f = 0; f < fields; f++)
|
|
179
|
+
values.push(convolveSiteKernelRuns(fieldSource(f), T, T, runs, input.pattern?.(f)));
|
|
180
|
+
cache.set(key, values);
|
|
181
|
+
if (cache.size > capacity)
|
|
182
|
+
cache.delete(cache.keys().next().value);
|
|
183
|
+
computed++;
|
|
184
|
+
if (input.onTile)
|
|
185
|
+
input.onTile(now() - started);
|
|
186
|
+
}
|
|
187
|
+
lastKey = key;
|
|
188
|
+
last = values;
|
|
189
|
+
return values;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
valueAt(field, x, y) {
|
|
193
|
+
const tx = Math.floor(x / T), ty = Math.floor(y / T);
|
|
194
|
+
return tileOf(tx, ty)[field][(y - ty * T) * T + x - tx * T];
|
|
195
|
+
},
|
|
196
|
+
tileAt: tileOf,
|
|
197
|
+
get tilesComputed() { return computed; }
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
//# sourceMappingURL=site-kernel-runs.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"site-kernel-runs.js","sourceRoot":"","sources":["../../src/optics/site-kernel-runs.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Large defocus kernels (#564): an exact convolution with a site kernel whose cost per output grows with the kernel's\n * perimeter, not its area, for kernels of radius above {@link DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES}.\n *\n * Method. Each kernel row is split into runs of consecutive taps with the same weight (run-length encoding). A run's\n * contribution to the output, w·Σ_{u = u0}^{u1} L(x − u, y − v), is the difference of two prefix sums of the source row\n * y − v, so it costs two reads however long the run is. Runs with the same weight are summed first and multiplied once.\n * This is the summed-area (integral image) evaluation of a box filter applied row by row: Crow, \"Summed-area tables for\n * texture mapping\", SIGGRAPH 1984 (ACM, free to read), and Heckbert, \"Filtering by repeated integration\", SIGGRAPH 1986\n * (free), which convolves any piecewise-constant kernel this way.\n *\n * The geometric defocus disc (`createDefocusDiscSiteKernel`) has one weight for every cell wholly inside the disc (the\n * cell's area is exactly 1, so its weight is 1 divided by the total, the same double for all of them); only the cells the\n * circle crosses, and the centre (which takes up the normalization's rounding), have their own weights. So the disc of\n * radius R costs one run per row (2R + 1 of them) plus about 8R single taps, against (2R + 1)² taps of the direct gather:\n * about 1,800 reads per output at R = 179 against 128,881. The shaped kernels of #553 without spherical aberration (blade\n * polygons, the cat's eye) have the same structure (a uniform interior, exact polygon areas at the edge); a kernel with\n * no equal neighbouring weights (spherical aberration's ray binning) has no runs and costs as much as the direct gather,\n * so {@link siteKernelRuns} refuses one whose reads per output exceed {@link SITE_KERNEL_RUNS_MAXIMUM_READS}.\n *\n * It is not an approximation: every output is the same sum of the same products as the direct gather, regrouped. The\n * regrouping changes only the rounding. The prefix sums of a row are accumulated from the left edge of the source tile,\n * so a run's difference carries an absolute rounding error of a few ulps of the row's prefix (at most the sum over the tile's\n * source row, (T + 2R) samples), and an output sums 2R + 1 run sums instead of (2R + 1)² products. Measured on the Siemens\n * star, slanted edge and dead leaves at radii 65, 179 and 512 against a compensated (Neumaier) sum of the same products: within\n * 2.5e-14 relative, where the direct gather itself was up to 1e-11 off (scripts/benchmark-large-defocus.mjs accuracy). Prefix\n * sums of non-negative samples never decrease, so every run sum, and every output, is non-negative.\n *\n * Window independence. Outputs are computed in fixed square tiles of T × T lattice points at absolute positions\n * (tile (tx, ty) holds [tx·T, (tx + 1)·T) × [ty·T, (ty + 1)·T)), each from its own source rectangle, the tile plus R on\n * every side. A value therefore depends only on its lattice point and the request, never on the window, region or order\n * of the queries: regions and parts are bit-identical to the whole frame.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/**\n * Largest kernel radius convolved directly ((2R + 1)² taps per output, the method of 2.16.0 and earlier). Requests within it\n * render byte-identically to 2.16.0; above it, the run convolution of this module applies.\n */\nexport const DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES = 64;\n\n/**\n * Most reads per output the run convolution accepts: the tap count of the direct gather at radius 64, (2·64 + 1)², so a large\n * kernel never costs more per output than the largest direct one. The disc stays far below it (6,145 at radius 512).\n */\nexport const SITE_KERNEL_RUNS_MAXIMUM_READS = 129 * 129;\n\n/** Shortest run summed by prefix differences; shorter runs are read tap by tap (fewer reads). */\nconst MINIMUM_RUN = 3;\nconst MAXIMUM_SAMPLE = 2 ** 100;\n\n/** A site kernel as weighted runs (prefix differences) and single taps, in a fixed order. */\nexport interface SiteKernelRuns {\n /** Kernel radius R: the window is (2R + 1)². */\n readonly radius: number;\n /** Distinct run weights, in order of first appearance (row-major). */\n readonly groupWeights: Float64Array;\n /** Runs of group g are runStart[g] … runStart[g + 1] − 1. */\n readonly runStart: Int32Array;\n /** Each run's kernel row v and columns u0 ≤ u1 (offsets from the centre). */\n readonly runV: Int32Array;\n readonly runU0: Int32Array;\n readonly runU1: Int32Array;\n /** Single taps: row v, column u, weight. */\n readonly tapV: Int32Array;\n readonly tapU: Int32Array;\n readonly tapW: Float64Array;\n /** Reads per output: 2 per run plus 1 per tap. */\n readonly readsPerOutput: number;\n}\n\n/**\n * The run decomposition of a site kernel of radius R ((2R + 1)² row-major weights, out(p) = Σ_t K(t)·L(p − t)). Zero taps are\n * skipped. Fails with `InvalidConfigurationError` when the kernel would need more than {@link SITE_KERNEL_RUNS_MAXIMUM_READS}\n * reads per output, with a short `lens.defocus:` message (the kernels it takes are `lens.defocus`'s; spherical aberration's ray binning\n * is the one that can reach the limit).\n */\nexport function siteKernelRuns(weights: Float64Array, radius: number): SiteKernelRuns {\n const width = 2 * radius + 1;\n if (!Number.isInteger(radius) || radius < 0 || weights.length !== width * width) throw new InvalidConfigurationError(\"Site kernels must be (2R + 1)² taps.\");\n const groups = new Map<number, number[]>(), tapV: number[] = [], tapU: number[] = [], tapW: number[] = [];\n for (let v = -radius; v <= radius; v++) {\n const row = (v + radius) * width;\n for (let u = -radius; u <= radius;) {\n const w = weights[row + u + radius]!;\n let end = u;\n while (end + 1 <= radius && weights[row + end + 1 + radius] === w) end++;\n if (w !== 0) {\n if (end - u + 1 >= MINIMUM_RUN) {\n let list = groups.get(w);\n if (!list) { list = []; groups.set(w, list); }\n list.push(v, u, end);\n } else for (let k = u; k <= end; k++) { tapV.push(v); tapU.push(k); tapW.push(w); }\n }\n u = end + 1;\n }\n }\n const groupWeights = Float64Array.from(groups.keys()), runStart = new Int32Array(groupWeights.length + 1), runV: number[] = [], runU0: number[] = [], runU1: number[] = [];\n let g = 0;\n for (const list of groups.values()) {\n for (let k = 0; k < list.length; k += 3) { runV.push(list[k]!); runU0.push(list[k + 1]!); runU1.push(list[k + 2]!); }\n runStart[++g] = runV.length;\n }\n const readsPerOutput = 2 * runV.length + tapW.length;\n if (readsPerOutput > SITE_KERNEL_RUNS_MAXIMUM_READS)\n throw new InvalidConfigurationError(\"lens.defocus: this blur needs \" + grouped(readsPerOutput) + \" reads per site; above a kernel radius of \" +\n DEFOCUS_DIRECT_KERNEL_MAXIMUM_RADIUS_SITES + \" sites the limit is \" + grouped(SITE_KERNEL_RUNS_MAXIMUM_READS) + \". Reduce the spherical aberration or the defocus.\");\n return { radius, groupWeights, runStart, runV: Int32Array.from(runV), runU0: Int32Array.from(runU0), runU1: Int32Array.from(runU1),\n tapV: Int32Array.from(tapV), tapU: Int32Array.from(tapU), tapW: Float64Array.from(tapW), readsPerOutput };\n}\n\n/** A non-negative integer with comma thousands separators (no locale data). */\nconst grouped = (value: number): string => String(value).replace(/\\B(?=(\\d{3})+$)/gu, \",\");\n\n/** Which outputs of a tile row to compute: columns start, start + step, … (tile coordinates); null skips the row. */\nexport type SiteKernelRunsRowPattern = (tileRow: number) => { start: number; step: number } | null;\n\n/**\n * Convolves a source rectangle with the runs: out(x, y) = Σ_t K(t)·source(x + R − t_u, y + R − t_v) for 0 ≤ x < width,\n * 0 ≤ y < height, the source holding (width + 2R) × (height + 2R) samples row-major. Each output is the sum over the weight\n * groups, in order, of the group weight times its runs' prefix differences (runs in order), then the single taps in order.\n * Outputs a `pattern` skips are 0. Samples are checked as `calculateIrradianceConvolution` checks them.\n */\nexport function convolveSiteKernelRuns(source: Float64Array, width: number, height: number, runs: SiteKernelRuns, pattern?: SiteKernelRunsRowPattern): Float64Array {\n const R = runs.radius, sw = width + 2 * R, sh = height + 2 * R, pw = sw + 1;\n if (source.length !== sw * sh) throw new InvalidConfigurationError(\"The run convolution's source must hold the tile and the kernel radius on every side.\");\n // Row prefix sums from the source tile's left edge: P(r, k) = Σ_{c < k} source(c, r).\n const prefix = new Float64Array(pw * sh);\n for (let r = 0; r < sh; r++) {\n let sum = 0;\n const s = r * sw, p = r * pw;\n for (let c = 0; c < sw; c++) {\n const value = source[s + c]!;\n if (!(value >= 0 && value <= MAXIMUM_SAMPLE)) throw new InvalidConfigurationError(\"Irradiance samples must be finite, non-negative and within the declared magnitude.\");\n sum += value;\n prefix[p + c + 1] = sum;\n }\n }\n const out = new Float64Array(width * height), group = new Float64Array(width);\n const { groupWeights, runStart, runV, runU0, runU1, tapV, tapU, tapW } = runs;\n for (let y = 0; y < height; y++) {\n const row = pattern ? pattern(y) : { start: 0, step: 1 };\n if (!row) continue;\n const { start, step } = row, o = y * width;\n for (let g = 0; g < groupWeights.length; g++) {\n group.fill(0);\n for (let k = runStart[g]!; k < runStart[g + 1]!; k++) {\n // Σ_{u = u0}^{u1} source(x + R − u, y + R − v) = P(y + R − v, x + R − u0 + 1) − P(y + R − v, x + R − u1).\n const p = (y + R - runV[k]!) * pw, hi = p + R - runU0[k]! + 1, lo = p + R - runU1[k]!;\n for (let x = start; x < width; x += step) group[x] = group[x]! + (prefix[hi + x]! - prefix[lo + x]!);\n }\n const w = groupWeights[g]!;\n for (let x = start; x < width; x += step) out[o + x] = out[o + x]! + w * group[x]!;\n }\n for (let k = 0; k < tapW.length; k++) {\n const s = (y + R - tapV[k]!) * sw + R - tapU[k]!, w = tapW[k]!;\n for (let x = start; x < width; x += step) out[o + x] = out[o + x]! + w * source[s + x]!;\n }\n }\n return out;\n}\n\n/**\n * The source of one tile: called with the source rectangle's origin and size (lattice coordinates; the tile plus the radius on\n * every side), it returns a function giving each field's samples, row-major, called once per field in order (so a caller can\n * share work between fields, such as one material lookup per point, and keep only one field's samples at a time).\n */\nexport type SiteKernelRunsTileSource = (x0: number, y0: number, width: number, height: number) => (field: number) => Float64Array;\n\nexport interface SiteKernelRunsLatticeInput {\n runs: SiteKernelRuns;\n /** Tile side T in lattice points (even, so CFA parity patterns are the same in every tile). */\n tile: number;\n fields: number;\n source: SiteKernelRunsTileSource;\n /** Per field, the outputs of each tile row to compute (absolute row parity is the tile row's, T being even). Default all. */\n pattern?: (field: number) => SiteKernelRunsRowPattern | undefined;\n /** Bytes of output tiles kept (least recently used dropped first). Default 64 MiB. */\n capacityBytes?: number;\n /** Called with the duration in ms of each tile (diagnostics only). */\n onTile?: (milliseconds: number) => void;\n}\n\n/** Convolved fields on a lattice, by fixed tiles (see the module comment), computed on first use and kept within a byte budget. */\nexport interface SiteKernelRunsLattice {\n /** Field `field` of the convolved lattice at lattice point (x, y) (any integers). */\n valueAt(field: number, x: number, y: number): number;\n /** The fields of tile (tx, ty): T × T values each, row-major, for lattice points [tx·T, (tx + 1)·T) × [ty·T, (ty + 1)·T). Read only. */\n tileAt(tx: number, ty: number): readonly Float64Array[];\n /** Tiles computed so far (diagnostics). */\n readonly tilesComputed: number;\n}\n\n/** Tile key: tiles within ±2^20 of the origin in each direction. */\nconst tileKey = (tx: number, ty: number): number => (ty + 1048576) * 2097152 + tx + 1048576;\n\nexport function createSiteKernelRunsLattice(input: SiteKernelRunsLatticeInput): SiteKernelRunsLattice {\n const { runs, tile: T, fields } = input, R = runs.radius;\n if (!Number.isInteger(T) || T < 2 || T % 2 !== 0) throw new InvalidConfigurationError(\"The run lattice's tile side must be an even integer of 2 or more.\");\n const capacity = Math.max(1, Math.floor((input.capacityBytes ?? 64 * 1048576) / (fields * T * T * 8)));\n const cache = new Map<number, Float64Array[]>();\n const now = (): number => globalThis.performance?.now() ?? Date.now();\n let computed = 0, lastKey = NaN, last: Float64Array[] | undefined;\n function tileOf(tx: number, ty: number): Float64Array[] {\n const key = tileKey(tx, ty);\n if (key === lastKey) return last!;\n let values = cache.get(key);\n if (values) { cache.delete(key); cache.set(key, values); }\n else {\n const started = input.onTile ? now() : 0;\n const fieldSource = input.source(tx * T - R, ty * T - R, T + 2 * R, T + 2 * R);\n values = [];\n for (let f = 0; f < fields; f++) values.push(convolveSiteKernelRuns(fieldSource(f), T, T, runs, input.pattern?.(f)));\n cache.set(key, values);\n if (cache.size > capacity) cache.delete(cache.keys().next().value!);\n computed++;\n if (input.onTile) input.onTile(now() - started);\n }\n lastKey = key; last = values;\n return values;\n }\n return {\n valueAt(field: number, x: number, y: number): number {\n const tx = Math.floor(x / T), ty = Math.floor(y / T);\n return tileOf(tx, ty)[field]![(y - ty * T) * T + x - tx * T]!;\n },\n tileAt: tileOf,\n get tilesComputed(): number { return computed; }\n };\n}\n"]}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type OpticalProfileState } from "../optics/profile-contract.js";
|
|
2
|
+
import { type GenericLensCorrectionProfile, type LensCorrectionChannel } from "./lens-corrections.js";
|
|
3
|
+
/**
|
|
4
|
+
* Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where
|
|
5
|
+
* a camera's channel responses peak, not a measured camera.
|
|
6
|
+
*/
|
|
7
|
+
export declare const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM: Readonly<Record<LensCorrectionChannel, number>>;
|
|
8
|
+
export interface CreateGenericLensCorrectionProfileInput {
|
|
9
|
+
/** A catalogue id (`GENERIC_LENS_IDS`); `state.lensId` must be the same id. */
|
|
10
|
+
lensId: string;
|
|
11
|
+
/** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */
|
|
12
|
+
activeImagingArea: {
|
|
13
|
+
widthMm: number;
|
|
14
|
+
heightMm: number;
|
|
15
|
+
};
|
|
16
|
+
/** The exact camera state the profile applies to: its focal length must be the lens's on this sensor, its aperture in range. */
|
|
17
|
+
state: OpticalProfileState;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own
|
|
21
|
+
* r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the
|
|
22
|
+
* relative-illumination gain at the state's aperture (strength 1). The result passes `parseGenericLensCorrectionProfile`.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createGenericLensCorrectionProfile(input: CreateGenericLensCorrectionProfileInput): GenericLensCorrectionProfile;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The generic correction profile of a catalogue lens (#549), for the post-capture lens corrections
|
|
4
|
+
* (`resolveLensCorrectionPlan`, `calculateLensCorrectedCapture`): a distortion map, a lateral-colour map per channel and a
|
|
5
|
+
* peripheral-illumination gain, each from the lens's own character (`resolveGenericLens`). Consumer lenses rely on it more:
|
|
6
|
+
* their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is
|
|
7
|
+
* a labelled approximation, bound to one exact camera state (`OpticalProfileState`).
|
|
8
|
+
*/
|
|
9
|
+
import { InvalidScientificInputError } from "../core/validation.js";
|
|
10
|
+
import { requireInputKeys, requireInputObject } from "../core/record-validation.js";
|
|
11
|
+
import { parseOpticalProfileState } from "../optics/profile-contract.js";
|
|
12
|
+
import { GENERIC_LENS_EVIDENCE, genericLensDispersion, resolveGenericLens } from "../equipment/generic-lens-catalogue.js";
|
|
13
|
+
import { parseGenericLensCorrectionProfile } from "./lens-corrections.js";
|
|
14
|
+
/**
|
|
15
|
+
* Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where
|
|
16
|
+
* a camera's channel responses peak, not a measured camera.
|
|
17
|
+
*/
|
|
18
|
+
export const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM = /* @__PURE__ */ Object.freeze({ red: 610, green: 540, blue: 465 });
|
|
19
|
+
/**
|
|
20
|
+
* Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own
|
|
21
|
+
* r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the
|
|
22
|
+
* relative-illumination gain at the state's aperture (strength 1). The result passes `parseGenericLensCorrectionProfile`.
|
|
23
|
+
*/
|
|
24
|
+
export function createGenericLensCorrectionProfile(input) {
|
|
25
|
+
requireInputObject(input, "createGenericLensCorrectionProfile input");
|
|
26
|
+
requireInputKeys(input, "createGenericLensCorrectionProfile", ["lensId", "activeImagingArea", "state"]);
|
|
27
|
+
const state = parseOpticalProfileState(input.state);
|
|
28
|
+
const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea, fNumber: state.aperture });
|
|
29
|
+
if (state.lensId !== lens.lensId)
|
|
30
|
+
throw new InvalidScientificInputError("state.lensId must be the catalogue id " + lens.lensId + ".");
|
|
31
|
+
if (Math.abs(state.focalLengthMm - lens.focalLengthMm) > 1e-9 * lens.focalLengthMm)
|
|
32
|
+
throw new InvalidScientificInputError("state.focalLengthMm must be the " + lens.name + "'s focal length on this sensor, " + lens.focalLengthMm + " mm.");
|
|
33
|
+
const c = lens.character, domain = "reconstructed-linear";
|
|
34
|
+
const identity = { version: "1.0.0", domain, availability: "toggle", defaultEnabled: true, dependencies: [], requiredByStabilizationModes: [] };
|
|
35
|
+
const scale = (channel) => 1 + c.lateralColor.fraction * genericLensDispersion(GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM[channel]);
|
|
36
|
+
const affine = (channel) => ({ id: lens.lensId + ":lateral-ca:" + channel, version: "1.0.0", domain, purpose: "lateral-ca", kind: "affine",
|
|
37
|
+
matrix: [scale(channel), 0, 0, scale(channel)], offsetMm: { x: 0, y: 0 } });
|
|
38
|
+
return parseGenericLensCorrectionProfile({ schemaVersion: "0.1.0", id: "photivra-" + lens.lensId + ":corrections", version: "1.0.0", state,
|
|
39
|
+
evidence: { kind: "generic-parametric", basis: "Generic catalogue lens " + lens.name + " (#549): the inverse of its own distortion, lateral colour and vignetting.",
|
|
40
|
+
residualNote: "Generic approximation, not a measured lens; axial colour, coma and the other aberrations stay.", sources: [...GENERIC_LENS_EVIDENCE] },
|
|
41
|
+
components: [
|
|
42
|
+
{ ...identity, id: "distortion", kind: "geometry", residualNote: "Corrects the lens's radial distortion r_d = r(1 + k1ρ²); the stretched corners lose resolution.",
|
|
43
|
+
transform: { id: lens.lensId + ":distortion", version: "1.0.0", domain, purpose: "distortion", kind: "radial",
|
|
44
|
+
profile: { normalizationRadiusMm: lens.halfDiagonalMm, maximumNormalizedRadius: 1.05, coefficients: { k1: c.distortion.corner, k2: 0, k3: 0 } } } },
|
|
45
|
+
{ ...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.",
|
|
46
|
+
transforms: { red: affine("red"), green: affine("green"), blue: affine("blue") } },
|
|
47
|
+
{ ...identity, id: "vignetting", kind: "peripheral-illumination", residualNote: "Gain 1/E at the profile's aperture; it raises corner noise with the signal.",
|
|
48
|
+
profile: { ...c.vignetting.profile, maximumNormalizedRadius: 1.05 }, strength: 1 }
|
|
49
|
+
] });
|
|
50
|
+
}
|
|
51
|
+
//# sourceMappingURL=generic-lens-corrections.js.map
|