@photivra/engine 2.18.0 → 2.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/focus-control.js +2 -75
- package/dist/capture/focus-control.js.map +1 -1
- package/dist/capture/native-environment-lean.d.ts +1 -0
- package/dist/capture/native-environment-lean.js +1 -1
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-optical-stage.d.ts +1 -0
- package/dist/capture/native-environment-optical-stage.js +2 -5
- package/dist/capture/native-environment-optical-stage.js.map +1 -1
- package/dist/capture/native-environment-raw.js +1 -5
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/release-sequence.js +2 -47
- package/dist/capture/release-sequence.js.map +1 -1
- package/dist/color/white-balance.d.ts +2 -0
- package/dist/color/white-balance.js +3 -56
- package/dist/color/white-balance.js.map +1 -1
- package/dist/composition/image-formation-plan.d.ts +1 -0
- package/dist/composition/image-formation-plan.js +3 -49
- package/dist/composition/image-formation-plan.js.map +1 -1
- package/dist/core/evidence-provenance.js +1 -16
- package/dist/core/evidence-provenance.js.map +1 -1
- package/dist/core/record-validation.d.ts +54 -0
- package/dist/core/record-validation.js +176 -0
- package/dist/core/record-validation.js.map +1 -1
- package/dist/core/scientific-assurance.js +1 -18
- package/dist/core/scientific-assurance.js.map +1 -1
- package/dist/core/shared-helpers.d.ts +33 -0
- package/dist/core/shared-helpers.js +34 -0
- package/dist/core/shared-helpers.js.map +1 -0
- package/dist/core/spectral-composition.js +1 -36
- package/dist/core/spectral-composition.js.map +1 -1
- package/dist/core/spectral.js +1 -19
- package/dist/core/spectral.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/exposure-capabilities.d.ts +1 -0
- package/dist/equipment/exposure-capabilities.js +2 -36
- package/dist/equipment/exposure-capabilities.js.map +1 -1
- package/dist/equipment/iso-capabilities.js +2 -49
- package/dist/equipment/iso-capabilities.js.map +1 -1
- package/dist/equipment/metering-capabilities.js +1 -26
- package/dist/equipment/metering-capabilities.js.map +1 -1
- package/dist/equipment/release-capabilities.js +1 -53
- package/dist/equipment/release-capabilities.js.map +1 -1
- package/dist/exposure/auto-iso-slowest-shutter.js +1 -7
- package/dist/exposure/auto-iso-slowest-shutter.js.map +1 -1
- package/dist/exposure/exposure-duration-control.js +1 -16
- package/dist/exposure/exposure-duration-control.js.map +1 -1
- package/dist/exposure/exposure-mode-resolver.js +1 -17
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/filtered-exposure-time.d.ts +93 -0
- package/dist/exposure/filtered-exposure-time.js +129 -0
- package/dist/exposure/filtered-exposure-time.js.map +1 -0
- package/dist/exposure/flash-ttl.d.ts +1 -0
- package/dist/exposure/flash-ttl.js +1 -1
- package/dist/exposure/flash-ttl.js.map +1 -1
- package/dist/exposure/flash.js +1 -49
- package/dist/exposure/flash.js.map +1 -1
- package/dist/exposure/metering-scene-radiance.js +1 -25
- package/dist/exposure/metering-scene-radiance.js.map +1 -1
- package/dist/exposure/metering-target.js +1 -30
- package/dist/exposure/metering-target.js.map +1 -1
- package/dist/exposure/metering-temporal.js +1 -8
- package/dist/exposure/metering-temporal.js.map +1 -1
- package/dist/exposure/metering.js +1 -72
- package/dist/exposure/metering.js.map +1 -1
- package/dist/exposure/program-line.js +1 -45
- package/dist/exposure/program-line.js.map +1 -1
- package/dist/facade/develop-joined-parts.d.ts +29 -0
- package/dist/facade/develop-joined-parts.js +33 -0
- package/dist/facade/develop-joined-parts.js.map +1 -0
- package/dist/facade/generic-lens-capture.d.ts +4 -2
- package/dist/facade/generic-lens-capture.js +4 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +37 -5
- package/dist/facade/simulate-capture.js +153 -96
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.js +1 -3
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +1 -5
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +13 -1
- package/dist/gpu/display-view-quality-gpu.js +4 -8
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.js +3 -25
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/sensor-stage-gpu.js +1 -5
- package/dist/gpu/sensor-stage-gpu.js.map +1 -1
- package/dist/gpu/webgpu-runtime.d.ts +2 -0
- package/dist/gpu/webgpu-runtime.js +3 -0
- package/dist/gpu/webgpu-runtime.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/motion/camera-rotation.js +1 -6
- package/dist/motion/camera-rotation.js.map +1 -1
- package/dist/motion/capture-rotation-inverse-mapping.d.ts +12 -0
- package/dist/motion/capture-rotation-inverse-mapping.js +2 -2
- package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-rotation-temporal-quadrature.js +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
- package/dist/motion/capture-rotation-trajectory.js +2 -17
- package/dist/motion/capture-rotation-trajectory.js.map +1 -1
- package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +1 -0
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js +3 -14
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
- package/dist/motion/extended-object-projection.js +5 -40
- package/dist/motion/extended-object-projection.js.map +1 -1
- package/dist/optics/atmospheric-haze.js +1 -7
- package/dist/optics/atmospheric-haze.js.map +1 -1
- package/dist/optics/brown-conrady-distortion.js +1 -7
- package/dist/optics/brown-conrady-distortion.js.map +1 -1
- package/dist/optics/complex-pupil-psf.d.ts +1 -0
- package/dist/optics/complex-pupil-psf.js +2 -84
- package/dist/optics/complex-pupil-psf.js.map +1 -1
- package/dist/optics/focus-state.js +1 -11
- package/dist/optics/focus-state.js.map +1 -1
- package/dist/optics/front-of-lens-filter.d.ts +5 -0
- package/dist/optics/front-of-lens-filter.js +3 -65
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/generic-nd-filter-presets.d.ts +12 -0
- package/dist/optics/generic-nd-filter-presets.js +48 -0
- package/dist/optics/generic-nd-filter-presets.js.map +1 -0
- package/dist/optics/illumination-vignetting.js +1 -6
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +4 -1
- package/dist/optics/lens-psf-profile.js +12 -111
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/longitudinal-chromatic-aberration.js +1 -7
- package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -1
- package/dist/optics/pupil-vignetting.js +1 -7
- package/dist/optics/pupil-vignetting.js.map +1 -1
- package/dist/optics/radial-distortion.js +1 -6
- package/dist/optics/radial-distortion.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.js +4 -96
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/stray-light.d.ts +1 -0
- package/dist/optics/stray-light.js +1 -1
- package/dist/optics/stray-light.js.map +1 -1
- package/dist/optics/zernike-wavefront.js +1 -7
- package/dist/optics/zernike-wavefront.js.map +1 -1
- package/dist/output/capture-geometry.js +1 -8
- package/dist/output/capture-geometry.js.map +1 -1
- package/dist/output/developed-lens-corrections.d.ts +62 -0
- package/dist/output/developed-lens-corrections.js +135 -0
- package/dist/output/developed-lens-corrections.js.map +1 -0
- package/dist/output/generic-lens-corrections.d.ts +2 -6
- package/dist/output/generic-lens-corrections.js +3 -6
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/geometric-transforms.js +1 -4
- package/dist/output/geometric-transforms.js.map +1 -1
- package/dist/output/native-capture-sdr.d.ts +2 -2
- package/dist/output/native-capture-sdr.js +3 -2
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/processing/development-detail.d.ts +29 -0
- package/dist/processing/development-detail.js +128 -107
- package/dist/processing/development-detail.js.map +1 -1
- package/dist/schema/illumination-temporal.js +3 -72
- package/dist/schema/illumination-temporal.js.map +1 -1
- package/dist/schema/illumination.d.ts +1 -0
- package/dist/schema/illumination.js +2 -65
- package/dist/schema/illumination.js.map +1 -1
- package/dist/schema/scene-radiance-temporal.js +2 -14
- package/dist/schema/scene-radiance-temporal.js.map +1 -1
- package/dist/schema/scene-radiance.js +2 -60
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/schema/validation.d.ts +2 -0
- package/dist/schema/validation.js +2 -17
- package/dist/schema/validation.js.map +1 -1
- package/dist/sensor/accumulated-charge.js +2 -59
- package/dist/sensor/accumulated-charge.js.map +1 -1
- package/dist/sensor/architecture.js +1 -16
- package/dist/sensor/architecture.js.map +1 -1
- package/dist/sensor/camera-saturation-capacity.d.ts +1 -0
- package/dist/sensor/camera-saturation-capacity.js +3 -55
- package/dist/sensor/camera-saturation-capacity.js.map +1 -1
- package/dist/sensor/capture-color-sampling-binding.js +1 -26
- package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
- package/dist/sensor/capture-mode-timing.js +1 -39
- package/dist/sensor/capture-mode-timing.js.map +1 -1
- package/dist/sensor/capture-mode.js +1 -23
- package/dist/sensor/capture-mode.js.map +1 -1
- package/dist/sensor/color-sampling.js +1 -26
- package/dist/sensor/color-sampling.js.map +1 -1
- package/dist/sensor/constant-rate-temporal-integration.js +2 -46
- package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
- package/dist/sensor/dark-current.js +2 -45
- package/dist/sensor/dark-current.js.map +1 -1
- package/dist/sensor/eqe-electron-rate.js +2 -35
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +5 -0
- package/dist/sensor/exposure-window.js +7 -22
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/iso-gain.js +1 -7
- package/dist/sensor/iso-gain.js.map +1 -1
- package/dist/sensor/iso-signal-chain.js +1 -43
- package/dist/sensor/iso-signal-chain.js.map +1 -1
- package/dist/sensor/optical-stack.js +1 -25
- package/dist/sensor/optical-stack.js.map +1 -1
- package/dist/sensor/photon-energy.js +1 -26
- package/dist/sensor/photon-energy.js.map +1 -1
- package/dist/sensor/physical-charge-capacity.js +3 -77
- package/dist/sensor/physical-charge-capacity.js.map +1 -1
- package/dist/sensor/radiometry-readiness.js +2 -23
- package/dist/sensor/radiometry-readiness.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +3 -0
- package/dist/sensor/raw-readout.js +4 -66
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/raw-reconstruction.js +2 -64
- package/dist/sensor/raw-reconstruction.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.js +1 -9
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/dist/sensor/readout-timing.js +3 -57
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.d.ts +2 -0
- package/dist/sensor/response-application-compatibility.js +3 -29
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/response-operating-range.d.ts +1 -0
- package/dist/sensor/response-operating-range.js +3 -79
- package/dist/sensor/response-operating-range.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.js +4 -108
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sampling-aperture.js +1 -25
- package/dist/sensor/sampling-aperture.js.map +1 -1
- package/dist/sensor/spatial-sample-reduction.js +2 -19
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spatial-sampling-quadrature.js +1 -3
- package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
- package/dist/sensor/spatio-spectral-reduction.js +1 -18
- package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
- package/dist/sensor/spectral-coverage.d.ts +10 -1
- package/dist/sensor/spectral-coverage.js +1 -1
- package/dist/sensor/spectral-coverage.js.map +1 -1
- package/dist/sensor/spectral-quadrature.js +2 -35
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/spectral-response.js +1 -47
- package/dist/sensor/spectral-response.js.map +1 -1
- package/dist/stabilization/camera-shake.js +1 -6
- package/dist/stabilization/camera-shake.js.map +1 -1
- package/dist/stabilization/system.js +2 -80
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +41 -2
- package/docs/QUICK_START.md +90 -8
- package/docs/USAGE.md +47 -2
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +53 -3
- package/docs/api/exposure-filtered-exposure-time.md +105 -0
- package/docs/api/facade-develop-joined-parts.md +32 -0
- package/docs/api/facade-simulate-capture.md +36 -5
- package/docs/api/optics-generic-nd-filter-presets.md +34 -0
- package/docs/api/optics-lens-psf-profile.md +3 -1
- package/docs/api/output-developed-lens-corrections.md +12 -0
- package/docs/api/output-generic-lens-corrections.md +0 -9
- package/llms.txt +1 -0
- package/package.json +1 -1
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { approximationResult } from "../core/calculation-result.js";
|
|
34
34
|
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
35
35
|
import { calculateDefocusWavefront } from "./zernike-wavefront.js";
|
|
36
|
-
import { requireInputKeys } from "../core/record-validation.js";
|
|
36
|
+
import { requireInputKeys, requireNamedFinite as requireFinite } from "../core/record-validation.js";
|
|
37
37
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
38
38
|
/** Fraunhofer spectral-line wavelengths in nm (SCHOTT TIE-29 table; also ISO 7944 reference wavelengths). */
|
|
39
39
|
export const FRAUNHOFER_D_LINE_NM = 587.5618;
|
|
@@ -41,12 +41,6 @@ export const FRAUNHOFER_F_LINE_NM = 486.1327;
|
|
|
41
41
|
export const FRAUNHOFER_C_LINE_NM = 656.2725;
|
|
42
42
|
const CAUCHY_MINIMUM_WAVELENGTH_NM = 360;
|
|
43
43
|
const CAUCHY_MAXIMUM_WAVELENGTH_NM = 830;
|
|
44
|
-
function requireFinite(name, value) {
|
|
45
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
46
|
-
throw new InvalidScientificInputError(`${name} must be finite.`);
|
|
47
|
-
}
|
|
48
|
-
return value;
|
|
49
|
-
}
|
|
50
44
|
function cauchyIndex(refractiveIndexD, abbeNumberD, wavelengthNm) {
|
|
51
45
|
const inverseSquare = (nm) => 1 / (nm / 1000) ** 2;
|
|
52
46
|
const b = (refractiveIndexD - 1) /
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"longitudinal-chromatic-aberration.js","sourceRoot":"","sources":["../../src/optics/longitudinal-chromatic-aberration.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Longitudinal (axial) chromatic aberration: wavelength-dependent focal length and the resulting\n * per-wavelength image-plane defocus for a sensor focused at a reference wavelength.\n *\n * Two dispersion inputs:\n * - thin-singlet-abbe: an ideal thin singlet whose index follows a two-term Cauchy dispersion\n * n(lambda) = A + B / lambda^2 fitted to n_d and the Abbe number V_d = (n_d - 1) / (n_F - n_C)\n * (Cauchy 1836; Oh 2023, arXiv:2309.09375, sec. 2.3.1(b), free; Abbe number: Greivenkamp,\n * OPTI-502 course notes, University of Arizona, sec. 17, free. Also see: Born & Wolf, Principles\n * of Optics, sec. 2.3; Hecht, Optics, 5th ed., sec. 3.5 and 6.3). Lensmaker scaling gives\n * f(lambda) = f_ref (n_ref - 1) / (n(lambda) - 1), so f_C - f_F ~ f_d / V_d (Greivenkamp,\n * OPTI-502 course notes, sec. 20, free. Also see: Hecht sec. 6.3.2; Kingslake & Johnson, Lens\n * Design Fundamentals, 2nd ed., ch. 5). The Fraunhofer d, F and C wavelengths (587.5618, 486.1327, 656.2725 nm) are standard\n * spectral-line constants (SCHOTT Technical Information TIE-29, 2023, free; also specified in\n * ISO 7944). A real multi-element lens is not a singlet, so this is an educational\n * approximation.\n * - declared-focal-shift: a caller-supplied table of focal-length shift versus wavelength\n * (for example a corrected lens's secondary spectrum), interpolated linearly without extrapolation.\n *\n * How defocus is computed:\n * - The sensor sits at the Gaussian thin-lens image distance for focusDistanceM at the reference\n * wavelength.\n * - At each wavelength, the image distance of subjectDistanceM uses f(lambda).\n * - The signed image-plane defocus dz is the sensor distance minus that image distance.\n * - The geometric blur diameter is D |dz| / v(lambda), where D = f_ref / N is the entrance-pupil\n * diameter. This is the same thin-lens form as calculateDefocusCircle().\n * The defocus also feeds calculateDefocusWavefront(), so a complex-pupil PSF (point-spread function)\n * can be evaluated per wavelength.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { calculateDefocusWavefront } from \"./zernike-wavefront.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Fraunhofer spectral-line wavelengths in nm (SCHOTT TIE-29 table; also ISO 7944 reference wavelengths). */\nexport const FRAUNHOFER_D_LINE_NM = 587.5618;\nexport const FRAUNHOFER_F_LINE_NM = 486.1327;\nexport const FRAUNHOFER_C_LINE_NM = 656.2725;\n\nconst CAUCHY_MINIMUM_WAVELENGTH_NM = 360;\nconst CAUCHY_MAXIMUM_WAVELENGTH_NM = 830;\n\nexport type LongitudinalChromaticDispersion =\n | {\n kind: \"thin-singlet-abbe\";\n /** Refractive index at the d line. */\n refractiveIndexD: number;\n /** Abbe number V_d = (n_d - 1) / (n_F - n_C). */\n abbeNumberD: number;\n }\n | {\n kind: \"declared-focal-shift\";\n /** Strictly increasing wavelengths; shift is f(lambda) - f_nominal in micrometres. */\n samples: readonly {\n wavelengthNm: number;\n focalLengthShiftMicrometers: number;\n }[];\n };\n\nexport interface CalculateLongitudinalChromaticFocusInput {\n /** Focal length at the reference wavelength, in mm. */\n focalLengthMm: number;\n /** Wavelength at which the sensor is focused, in nm. */\n referenceWavelengthNm: number;\n /** Wavelength to evaluate, in nm (same wavelength basis as the dispersion data). */\n wavelengthNm: number;\n /** Nominal f-number at the reference wavelength (entrance-pupil diameter = f / N). */\n apertureFNumber: number;\n /** Focus (object) distance at the reference wavelength, metres from the principal plane. */\n focusDistanceM: number;\n /** Subject distance in metres; defaults to focusDistanceM. */\n subjectDistanceM?: number;\n dispersion: LongitudinalChromaticDispersion;\n}\n\nexport interface LongitudinalChromaticFocus {\n wavelengthNm: number;\n referenceWavelengthNm: number;\n focalLengthAtWavelengthMm: number;\n /** f(lambda) - f(reference), micrometres. */\n focalShiftMicrometers: number;\n /** Sensor plane distance (reference-wavelength image of the focus plane), mm. */\n sensorImageDistanceMm: number;\n /** Image distance of the subject at this wavelength, mm. */\n imageDistanceAtWavelengthMm: number;\n /** Sensor minus image distance, micrometres. */\n signedDefocusImagePlaneMicrometers: number;\n /** Geometric thin-lens blur-circle diameter at the sensor, mm. */\n geometricBlurDiameterMm: number;\n /** Image-side working f-number v(lambda) / D. */\n workingFNumberAtWavelength: number;\n /** Defocus wave aberration W020 for the complex-pupil PSF, micrometres. */\n w020Micrometers: number;\n /** Equivalent Noll Z4 coefficient, micrometres. */\n zernikeZ4Micrometers: number;\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n return value;\n}\n\nfunction cauchyIndex(\n refractiveIndexD: number,\n abbeNumberD: number,\n wavelengthNm: number\n): number {\n const inverseSquare = (nm: number): number => 1 / (nm / 1000) ** 2;\n const b =\n (refractiveIndexD - 1) /\n abbeNumberD /\n (inverseSquare(FRAUNHOFER_F_LINE_NM) - inverseSquare(FRAUNHOFER_C_LINE_NM));\n return (\n refractiveIndexD +\n b * (inverseSquare(wavelengthNm) - inverseSquare(FRAUNHOFER_D_LINE_NM))\n );\n}\n\n/**\n * Two-term Cauchy refractive index fitted to n_d and V_d. Valid as an approximation over\n * 360-830 nm only.\n */\nexport function calculateCauchyRefractiveIndex(input: {\n refractiveIndexD: number;\n abbeNumberD: number;\n wavelengthNm: number;\n}): CalculationResult<{ refractiveIndex: number }> {\n requireInputKeys(input, \"calculateCauchyRefractiveIndex\", [\"refractiveIndexD\", \"abbeNumberD\", \"wavelengthNm\"]);\n validateAbbe(input.refractiveIndexD, input.abbeNumberD);\n validateCauchyWavelength(\"wavelengthNm\", input.wavelengthNm);\n return approximationResult(\n {\n refractiveIndex: cauchyIndex(\n input.refractiveIndexD,\n input.abbeNumberD,\n input.wavelengthNm\n )\n },\n \"two-term-cauchy-abbe-dispersion\",\n \"1.0.0\",\n [\n \"n(lambda) = A + B / lambda^2 fitted exactly to n_d and V_d = (n_d - 1) / (n_F - n_C)\",\n \"Two-term Cauchy dispersion is an approximation for normal dispersion in the visible (360-830 nm)\"\n ]\n );\n}\n\nfunction validateAbbe(refractiveIndexD: unknown, abbeNumberD: unknown): void {\n const n = requireFinite(\"dispersion.refractiveIndexD\", refractiveIndexD);\n const v = requireFinite(\"dispersion.abbeNumberD\", abbeNumberD);\n if (n <= 1 || n > 4) {\n throw new InvalidScientificInputError(\n \"dispersion.refractiveIndexD must lie in (1, 4].\"\n );\n }\n if (v < 5 || v > 200) {\n throw new InvalidScientificInputError(\n \"dispersion.abbeNumberD must lie in [5, 200].\"\n );\n }\n}\n\nfunction validateCauchyWavelength(name: string, value: unknown): void {\n const wavelength = requireFinite(name, value);\n if (\n wavelength < CAUCHY_MINIMUM_WAVELENGTH_NM ||\n wavelength > CAUCHY_MAXIMUM_WAVELENGTH_NM\n ) {\n throw new InvalidScientificInputError(\n `${name} must lie in ${CAUCHY_MINIMUM_WAVELENGTH_NM}-${CAUCHY_MAXIMUM_WAVELENGTH_NM} nm for the Cauchy dispersion approximation.`\n );\n }\n}\n\nfunction interpolateShift(\n samples: readonly { wavelengthNm: number; focalLengthShiftMicrometers: number }[],\n wavelengthNm: number,\n name: string\n): number {\n const first = samples[0]!;\n const last = samples[samples.length - 1]!;\n if (wavelengthNm < first.wavelengthNm || wavelengthNm > last.wavelengthNm) {\n throw new InvalidScientificInputError(\n `${name} lies outside the declared focal-shift wavelength range; extrapolation is not performed.`\n );\n }\n for (let index = 1; index < samples.length; index += 1) {\n const right = samples[index]!;\n if (wavelengthNm <= right.wavelengthNm) {\n const left = samples[index - 1]!;\n const t = (wavelengthNm - left.wavelengthNm) / (right.wavelengthNm - left.wavelengthNm);\n return (\n left.focalLengthShiftMicrometers +\n t * (right.focalLengthShiftMicrometers - left.focalLengthShiftMicrometers)\n );\n }\n }\n return first.focalLengthShiftMicrometers;\n}\n\nfunction parseSamples(\n value: unknown\n): readonly { wavelengthNm: number; focalLengthShiftMicrometers: number }[] {\n if (!Array.isArray(value) || value.length < 2 || value.length > 1024) {\n throw new InvalidScientificInputError(\n \"dispersion.samples must contain 2 through 1024 entries.\"\n );\n }\n let previous = 0;\n return value.map((entry: unknown, index) => {\n if (typeof entry !== \"object\" || entry === null) {\n throw new InvalidConfigurationError(`dispersion.samples[${index}] must be an object.`);\n }\n const record = entry as Record<string, unknown>;\n const wavelengthNm = requireFinite(`dispersion.samples[${index}].wavelengthNm`, record.wavelengthNm);\n if (wavelengthNm <= previous) {\n throw new InvalidScientificInputError(\n \"dispersion.samples wavelengths must be positive and strictly increasing.\"\n );\n }\n previous = wavelengthNm;\n return {\n wavelengthNm,\n focalLengthShiftMicrometers: requireFinite(\n `dispersion.samples[${index}].focalLengthShiftMicrometers`,\n record.focalLengthShiftMicrometers\n )\n };\n });\n}\n\nfunction imageDistanceMm(focalLengthMm: number, objectDistanceM: number): number {\n const objectMm = objectDistanceM * 1000;\n if (objectMm <= focalLengthMm) {\n throw new InvalidScientificInputError(\n \"Object distances must lie beyond the wavelength-dependent focal length.\"\n );\n }\n return (focalLengthMm * objectMm) / (objectMm - focalLengthMm);\n}\n\n/**\n * Wavelength-dependent focus for one wavelength: focal length, image distance, signed defocus at\n * a sensor focused at the reference wavelength, geometric blur and the defocus wavefront.\n */\nexport function calculateLongitudinalChromaticFocus(\n input: CalculateLongitudinalChromaticFocusInput\n): CalculationResult<LongitudinalChromaticFocus> {\n requireInputKeys(input, \"calculateLongitudinalChromaticFocus\", [\"focalLengthMm\", \"referenceWavelengthNm\", \"wavelengthNm\", \"apertureFNumber\", \"focusDistanceM\", \"subjectDistanceM\", \"dispersion\"]);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n requirePositiveFinite(\"referenceWavelengthNm\", input.referenceWavelengthNm);\n requirePositiveFinite(\"wavelengthNm\", input.wavelengthNm);\n requirePositiveFinite(\"apertureFNumber\", input.apertureFNumber);\n requirePositiveFinite(\"focusDistanceM\", input.focusDistanceM);\n const subjectDistanceM = input.subjectDistanceM ?? input.focusDistanceM;\n requirePositiveFinite(\"subjectDistanceM\", subjectDistanceM);\n const dispersion = input.dispersion;\n if (typeof dispersion !== \"object\" || dispersion === null) {\n throw new InvalidConfigurationError(\"dispersion must be an object.\");\n }\n\n let focalLengthAtWavelengthMm: number;\n let model: string;\n let dispersionAssumption: string;\n if (dispersion.kind === \"thin-singlet-abbe\") {\n validateAbbe(dispersion.refractiveIndexD, dispersion.abbeNumberD);\n validateCauchyWavelength(\"referenceWavelengthNm\", input.referenceWavelengthNm);\n validateCauchyWavelength(\"wavelengthNm\", input.wavelengthNm);\n const nRef = cauchyIndex(dispersion.refractiveIndexD, dispersion.abbeNumberD, input.referenceWavelengthNm);\n const nLambda = cauchyIndex(dispersion.refractiveIndexD, dispersion.abbeNumberD, input.wavelengthNm);\n focalLengthAtWavelengthMm = (input.focalLengthMm * (nRef - 1)) / (nLambda - 1);\n model = \"thin-singlet-abbe-longitudinal-chromatic-focus\";\n dispersionAssumption =\n \"Ideal thin singlet with two-term Cauchy dispersion fitted to n_d and V_d; f(lambda) = f_ref (n_ref - 1) / (n(lambda) - 1)\";\n } else if (dispersion.kind === \"declared-focal-shift\") {\n const samples = parseSamples(dispersion.samples);\n const shiftRef = interpolateShift(samples, input.referenceWavelengthNm, \"referenceWavelengthNm\");\n const shift = interpolateShift(samples, input.wavelengthNm, \"wavelengthNm\");\n focalLengthAtWavelengthMm = input.focalLengthMm + (shift - shiftRef) / 1000;\n model = \"declared-focal-shift-longitudinal-chromatic-focus\";\n dispersionAssumption =\n \"Caller-declared focal-length shift table, linearly interpolated and re-referenced to the reference wavelength; no extrapolation\";\n } else {\n throw new InvalidConfigurationError(\"dispersion.kind is invalid.\");\n }\n if (!(focalLengthAtWavelengthMm > 0) || !Number.isFinite(focalLengthAtWavelengthMm)) {\n throw new InvalidScientificInputError(\n \"Wavelength-dependent focal length must remain finite and positive.\"\n );\n }\n\n const sensorImageDistanceMm = imageDistanceMm(input.focalLengthMm, input.focusDistanceM);\n const imageDistanceAtWavelengthMm = imageDistanceMm(focalLengthAtWavelengthMm, subjectDistanceM);\n const pupilDiameterMm = input.focalLengthMm / input.apertureFNumber;\n const defocusMm = sensorImageDistanceMm - imageDistanceAtWavelengthMm;\n const workingFNumberAtWavelength = imageDistanceAtWavelengthMm / pupilDiameterMm;\n const wavefront = calculateDefocusWavefront({\n signedDefocusImagePlaneMicrometers: defocusMm * 1000,\n workingFNumber: workingFNumberAtWavelength\n }).value;\n\n return approximationResult(\n {\n wavelengthNm: input.wavelengthNm,\n referenceWavelengthNm: input.referenceWavelengthNm,\n focalLengthAtWavelengthMm,\n focalShiftMicrometers: (focalLengthAtWavelengthMm - input.focalLengthMm) * 1000,\n sensorImageDistanceMm,\n imageDistanceAtWavelengthMm,\n signedDefocusImagePlaneMicrometers: defocusMm * 1000,\n geometricBlurDiameterMm:\n (pupilDiameterMm * Math.abs(defocusMm)) / imageDistanceAtWavelengthMm,\n workingFNumberAtWavelength,\n w020Micrometers: wavefront.w020Micrometers,\n zernikeZ4Micrometers: wavefront.zernikeZ4Micrometers\n },\n model,\n \"1.0.0\",\n [\n dispersionAssumption,\n \"Gaussian thin-lens imaging; the sensor is focused on focusDistanceM at the reference wavelength\",\n \"Entrance-pupil diameter f_ref / N is wavelength independent; distances are measured from ideal principal planes\",\n \"Longitudinal chromatic focus only: lateral chromatic magnification, spherochromatism and sensor response are separate\",\n \"Wavelength basis must match the dispersion data; no air/vacuum conversion is performed\"\n ]\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"longitudinal-chromatic-aberration.js","sourceRoot":"","sources":["../../src/optics/longitudinal-chromatic-aberration.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Longitudinal (axial) chromatic aberration: wavelength-dependent focal length and the resulting\n * per-wavelength image-plane defocus for a sensor focused at a reference wavelength.\n *\n * Two dispersion inputs:\n * - thin-singlet-abbe: an ideal thin singlet whose index follows a two-term Cauchy dispersion\n * n(lambda) = A + B / lambda^2 fitted to n_d and the Abbe number V_d = (n_d - 1) / (n_F - n_C)\n * (Cauchy 1836; Oh 2023, arXiv:2309.09375, sec. 2.3.1(b), free; Abbe number: Greivenkamp,\n * OPTI-502 course notes, University of Arizona, sec. 17, free. Also see: Born & Wolf, Principles\n * of Optics, sec. 2.3; Hecht, Optics, 5th ed., sec. 3.5 and 6.3). Lensmaker scaling gives\n * f(lambda) = f_ref (n_ref - 1) / (n(lambda) - 1), so f_C - f_F ~ f_d / V_d (Greivenkamp,\n * OPTI-502 course notes, sec. 20, free. Also see: Hecht sec. 6.3.2; Kingslake & Johnson, Lens\n * Design Fundamentals, 2nd ed., ch. 5). The Fraunhofer d, F and C wavelengths (587.5618, 486.1327, 656.2725 nm) are standard\n * spectral-line constants (SCHOTT Technical Information TIE-29, 2023, free; also specified in\n * ISO 7944). A real multi-element lens is not a singlet, so this is an educational\n * approximation.\n * - declared-focal-shift: a caller-supplied table of focal-length shift versus wavelength\n * (for example a corrected lens's secondary spectrum), interpolated linearly without extrapolation.\n *\n * How defocus is computed:\n * - The sensor sits at the Gaussian thin-lens image distance for focusDistanceM at the reference\n * wavelength.\n * - At each wavelength, the image distance of subjectDistanceM uses f(lambda).\n * - The signed image-plane defocus dz is the sensor distance minus that image distance.\n * - The geometric blur diameter is D |dz| / v(lambda), where D = f_ref / N is the entrance-pupil\n * diameter. This is the same thin-lens form as calculateDefocusCircle().\n * The defocus also feeds calculateDefocusWavefront(), so a complex-pupil PSF (point-spread function)\n * can be evaluated per wavelength.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { calculateDefocusWavefront } from \"./zernike-wavefront.js\";\nimport { requireInputKeys, requireNamedFinite as requireFinite } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/** Fraunhofer spectral-line wavelengths in nm (SCHOTT TIE-29 table; also ISO 7944 reference wavelengths). */\nexport const FRAUNHOFER_D_LINE_NM = 587.5618;\nexport const FRAUNHOFER_F_LINE_NM = 486.1327;\nexport const FRAUNHOFER_C_LINE_NM = 656.2725;\n\nconst CAUCHY_MINIMUM_WAVELENGTH_NM = 360;\nconst CAUCHY_MAXIMUM_WAVELENGTH_NM = 830;\n\nexport type LongitudinalChromaticDispersion =\n | {\n kind: \"thin-singlet-abbe\";\n /** Refractive index at the d line. */\n refractiveIndexD: number;\n /** Abbe number V_d = (n_d - 1) / (n_F - n_C). */\n abbeNumberD: number;\n }\n | {\n kind: \"declared-focal-shift\";\n /** Strictly increasing wavelengths; shift is f(lambda) - f_nominal in micrometres. */\n samples: readonly {\n wavelengthNm: number;\n focalLengthShiftMicrometers: number;\n }[];\n };\n\nexport interface CalculateLongitudinalChromaticFocusInput {\n /** Focal length at the reference wavelength, in mm. */\n focalLengthMm: number;\n /** Wavelength at which the sensor is focused, in nm. */\n referenceWavelengthNm: number;\n /** Wavelength to evaluate, in nm (same wavelength basis as the dispersion data). */\n wavelengthNm: number;\n /** Nominal f-number at the reference wavelength (entrance-pupil diameter = f / N). */\n apertureFNumber: number;\n /** Focus (object) distance at the reference wavelength, metres from the principal plane. */\n focusDistanceM: number;\n /** Subject distance in metres; defaults to focusDistanceM. */\n subjectDistanceM?: number;\n dispersion: LongitudinalChromaticDispersion;\n}\n\nexport interface LongitudinalChromaticFocus {\n wavelengthNm: number;\n referenceWavelengthNm: number;\n focalLengthAtWavelengthMm: number;\n /** f(lambda) - f(reference), micrometres. */\n focalShiftMicrometers: number;\n /** Sensor plane distance (reference-wavelength image of the focus plane), mm. */\n sensorImageDistanceMm: number;\n /** Image distance of the subject at this wavelength, mm. */\n imageDistanceAtWavelengthMm: number;\n /** Sensor minus image distance, micrometres. */\n signedDefocusImagePlaneMicrometers: number;\n /** Geometric thin-lens blur-circle diameter at the sensor, mm. */\n geometricBlurDiameterMm: number;\n /** Image-side working f-number v(lambda) / D. */\n workingFNumberAtWavelength: number;\n /** Defocus wave aberration W020 for the complex-pupil PSF, micrometres. */\n w020Micrometers: number;\n /** Equivalent Noll Z4 coefficient, micrometres. */\n zernikeZ4Micrometers: number;\n}\n\nfunction cauchyIndex(\n refractiveIndexD: number,\n abbeNumberD: number,\n wavelengthNm: number\n): number {\n const inverseSquare = (nm: number): number => 1 / (nm / 1000) ** 2;\n const b =\n (refractiveIndexD - 1) /\n abbeNumberD /\n (inverseSquare(FRAUNHOFER_F_LINE_NM) - inverseSquare(FRAUNHOFER_C_LINE_NM));\n return (\n refractiveIndexD +\n b * (inverseSquare(wavelengthNm) - inverseSquare(FRAUNHOFER_D_LINE_NM))\n );\n}\n\n/**\n * Two-term Cauchy refractive index fitted to n_d and V_d. Valid as an approximation over\n * 360-830 nm only.\n */\nexport function calculateCauchyRefractiveIndex(input: {\n refractiveIndexD: number;\n abbeNumberD: number;\n wavelengthNm: number;\n}): CalculationResult<{ refractiveIndex: number }> {\n requireInputKeys(input, \"calculateCauchyRefractiveIndex\", [\"refractiveIndexD\", \"abbeNumberD\", \"wavelengthNm\"]);\n validateAbbe(input.refractiveIndexD, input.abbeNumberD);\n validateCauchyWavelength(\"wavelengthNm\", input.wavelengthNm);\n return approximationResult(\n {\n refractiveIndex: cauchyIndex(\n input.refractiveIndexD,\n input.abbeNumberD,\n input.wavelengthNm\n )\n },\n \"two-term-cauchy-abbe-dispersion\",\n \"1.0.0\",\n [\n \"n(lambda) = A + B / lambda^2 fitted exactly to n_d and V_d = (n_d - 1) / (n_F - n_C)\",\n \"Two-term Cauchy dispersion is an approximation for normal dispersion in the visible (360-830 nm)\"\n ]\n );\n}\n\nfunction validateAbbe(refractiveIndexD: unknown, abbeNumberD: unknown): void {\n const n = requireFinite(\"dispersion.refractiveIndexD\", refractiveIndexD);\n const v = requireFinite(\"dispersion.abbeNumberD\", abbeNumberD);\n if (n <= 1 || n > 4) {\n throw new InvalidScientificInputError(\n \"dispersion.refractiveIndexD must lie in (1, 4].\"\n );\n }\n if (v < 5 || v > 200) {\n throw new InvalidScientificInputError(\n \"dispersion.abbeNumberD must lie in [5, 200].\"\n );\n }\n}\n\nfunction validateCauchyWavelength(name: string, value: unknown): void {\n const wavelength = requireFinite(name, value);\n if (\n wavelength < CAUCHY_MINIMUM_WAVELENGTH_NM ||\n wavelength > CAUCHY_MAXIMUM_WAVELENGTH_NM\n ) {\n throw new InvalidScientificInputError(\n `${name} must lie in ${CAUCHY_MINIMUM_WAVELENGTH_NM}-${CAUCHY_MAXIMUM_WAVELENGTH_NM} nm for the Cauchy dispersion approximation.`\n );\n }\n}\n\nfunction interpolateShift(\n samples: readonly { wavelengthNm: number; focalLengthShiftMicrometers: number }[],\n wavelengthNm: number,\n name: string\n): number {\n const first = samples[0]!;\n const last = samples[samples.length - 1]!;\n if (wavelengthNm < first.wavelengthNm || wavelengthNm > last.wavelengthNm) {\n throw new InvalidScientificInputError(\n `${name} lies outside the declared focal-shift wavelength range; extrapolation is not performed.`\n );\n }\n for (let index = 1; index < samples.length; index += 1) {\n const right = samples[index]!;\n if (wavelengthNm <= right.wavelengthNm) {\n const left = samples[index - 1]!;\n const t = (wavelengthNm - left.wavelengthNm) / (right.wavelengthNm - left.wavelengthNm);\n return (\n left.focalLengthShiftMicrometers +\n t * (right.focalLengthShiftMicrometers - left.focalLengthShiftMicrometers)\n );\n }\n }\n return first.focalLengthShiftMicrometers;\n}\n\nfunction parseSamples(\n value: unknown\n): readonly { wavelengthNm: number; focalLengthShiftMicrometers: number }[] {\n if (!Array.isArray(value) || value.length < 2 || value.length > 1024) {\n throw new InvalidScientificInputError(\n \"dispersion.samples must contain 2 through 1024 entries.\"\n );\n }\n let previous = 0;\n return value.map((entry: unknown, index) => {\n if (typeof entry !== \"object\" || entry === null) {\n throw new InvalidConfigurationError(`dispersion.samples[${index}] must be an object.`);\n }\n const record = entry as Record<string, unknown>;\n const wavelengthNm = requireFinite(`dispersion.samples[${index}].wavelengthNm`, record.wavelengthNm);\n if (wavelengthNm <= previous) {\n throw new InvalidScientificInputError(\n \"dispersion.samples wavelengths must be positive and strictly increasing.\"\n );\n }\n previous = wavelengthNm;\n return {\n wavelengthNm,\n focalLengthShiftMicrometers: requireFinite(\n `dispersion.samples[${index}].focalLengthShiftMicrometers`,\n record.focalLengthShiftMicrometers\n )\n };\n });\n}\n\nfunction imageDistanceMm(focalLengthMm: number, objectDistanceM: number): number {\n const objectMm = objectDistanceM * 1000;\n if (objectMm <= focalLengthMm) {\n throw new InvalidScientificInputError(\n \"Object distances must lie beyond the wavelength-dependent focal length.\"\n );\n }\n return (focalLengthMm * objectMm) / (objectMm - focalLengthMm);\n}\n\n/**\n * Wavelength-dependent focus for one wavelength: focal length, image distance, signed defocus at\n * a sensor focused at the reference wavelength, geometric blur and the defocus wavefront.\n */\nexport function calculateLongitudinalChromaticFocus(\n input: CalculateLongitudinalChromaticFocusInput\n): CalculationResult<LongitudinalChromaticFocus> {\n requireInputKeys(input, \"calculateLongitudinalChromaticFocus\", [\"focalLengthMm\", \"referenceWavelengthNm\", \"wavelengthNm\", \"apertureFNumber\", \"focusDistanceM\", \"subjectDistanceM\", \"dispersion\"]);\n requirePositiveFinite(\"focalLengthMm\", input.focalLengthMm);\n requirePositiveFinite(\"referenceWavelengthNm\", input.referenceWavelengthNm);\n requirePositiveFinite(\"wavelengthNm\", input.wavelengthNm);\n requirePositiveFinite(\"apertureFNumber\", input.apertureFNumber);\n requirePositiveFinite(\"focusDistanceM\", input.focusDistanceM);\n const subjectDistanceM = input.subjectDistanceM ?? input.focusDistanceM;\n requirePositiveFinite(\"subjectDistanceM\", subjectDistanceM);\n const dispersion = input.dispersion;\n if (typeof dispersion !== \"object\" || dispersion === null) {\n throw new InvalidConfigurationError(\"dispersion must be an object.\");\n }\n\n let focalLengthAtWavelengthMm: number;\n let model: string;\n let dispersionAssumption: string;\n if (dispersion.kind === \"thin-singlet-abbe\") {\n validateAbbe(dispersion.refractiveIndexD, dispersion.abbeNumberD);\n validateCauchyWavelength(\"referenceWavelengthNm\", input.referenceWavelengthNm);\n validateCauchyWavelength(\"wavelengthNm\", input.wavelengthNm);\n const nRef = cauchyIndex(dispersion.refractiveIndexD, dispersion.abbeNumberD, input.referenceWavelengthNm);\n const nLambda = cauchyIndex(dispersion.refractiveIndexD, dispersion.abbeNumberD, input.wavelengthNm);\n focalLengthAtWavelengthMm = (input.focalLengthMm * (nRef - 1)) / (nLambda - 1);\n model = \"thin-singlet-abbe-longitudinal-chromatic-focus\";\n dispersionAssumption =\n \"Ideal thin singlet with two-term Cauchy dispersion fitted to n_d and V_d; f(lambda) = f_ref (n_ref - 1) / (n(lambda) - 1)\";\n } else if (dispersion.kind === \"declared-focal-shift\") {\n const samples = parseSamples(dispersion.samples);\n const shiftRef = interpolateShift(samples, input.referenceWavelengthNm, \"referenceWavelengthNm\");\n const shift = interpolateShift(samples, input.wavelengthNm, \"wavelengthNm\");\n focalLengthAtWavelengthMm = input.focalLengthMm + (shift - shiftRef) / 1000;\n model = \"declared-focal-shift-longitudinal-chromatic-focus\";\n dispersionAssumption =\n \"Caller-declared focal-length shift table, linearly interpolated and re-referenced to the reference wavelength; no extrapolation\";\n } else {\n throw new InvalidConfigurationError(\"dispersion.kind is invalid.\");\n }\n if (!(focalLengthAtWavelengthMm > 0) || !Number.isFinite(focalLengthAtWavelengthMm)) {\n throw new InvalidScientificInputError(\n \"Wavelength-dependent focal length must remain finite and positive.\"\n );\n }\n\n const sensorImageDistanceMm = imageDistanceMm(input.focalLengthMm, input.focusDistanceM);\n const imageDistanceAtWavelengthMm = imageDistanceMm(focalLengthAtWavelengthMm, subjectDistanceM);\n const pupilDiameterMm = input.focalLengthMm / input.apertureFNumber;\n const defocusMm = sensorImageDistanceMm - imageDistanceAtWavelengthMm;\n const workingFNumberAtWavelength = imageDistanceAtWavelengthMm / pupilDiameterMm;\n const wavefront = calculateDefocusWavefront({\n signedDefocusImagePlaneMicrometers: defocusMm * 1000,\n workingFNumber: workingFNumberAtWavelength\n }).value;\n\n return approximationResult(\n {\n wavelengthNm: input.wavelengthNm,\n referenceWavelengthNm: input.referenceWavelengthNm,\n focalLengthAtWavelengthMm,\n focalShiftMicrometers: (focalLengthAtWavelengthMm - input.focalLengthMm) * 1000,\n sensorImageDistanceMm,\n imageDistanceAtWavelengthMm,\n signedDefocusImagePlaneMicrometers: defocusMm * 1000,\n geometricBlurDiameterMm:\n (pupilDiameterMm * Math.abs(defocusMm)) / imageDistanceAtWavelengthMm,\n workingFNumberAtWavelength,\n w020Micrometers: wavefront.w020Micrometers,\n zernikeZ4Micrometers: wavefront.zernikeZ4Micrometers\n },\n model,\n \"1.0.0\",\n [\n dispersionAssumption,\n \"Gaussian thin-lens imaging; the sensor is focused on focusDistanceM at the reference wavelength\",\n \"Entrance-pupil diameter f_ref / N is wavelength independent; distances are measured from ideal principal planes\",\n \"Longitudinal chromatic focus only: lateral chromatic magnification, spherochromatism and sensor response are separate\",\n \"Wavelength basis must match the dispersion data; no air/vacuum conversion is performed\"\n ]\n );\n}\n"]}
|
|
@@ -40,14 +40,8 @@
|
|
|
40
40
|
*/
|
|
41
41
|
import { approximationResult, calculatedResult } from "../core/calculation-result.js";
|
|
42
42
|
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
43
|
-
import { requireInputKeys } from "../core/record-validation.js";
|
|
43
|
+
import { requireInputKeys, requireNamedFinite as requireFinite } from "../core/record-validation.js";
|
|
44
44
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
45
|
-
function requireFinite(name, value) {
|
|
46
|
-
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
47
|
-
throw new InvalidScientificInputError(`${name} must be finite.`);
|
|
48
|
-
}
|
|
49
|
-
return value;
|
|
50
|
-
}
|
|
51
45
|
/**
|
|
52
46
|
* Natural (cos^4 or finite Lambertian pupil) relative illumination at one image point.
|
|
53
47
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pupil-vignetting.js","sourceRoot":"","sources":["../../src/optics/pupil-vignetting.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Physical vignetting models derived from pupil geometry. They are separate from the\n * calculateIlluminationVignetting() polynomial, whose coefficients the caller supplies.\n *\n * Apply at most one field-throughput model to a signal. Do not stack these results on top of a\n * supplied field-throughput profile.\n *\n * Natural vignetting (relative illumination of an aberration-free system with a uniform Lambertian\n * exit pupil):\n * - cosine-fourth: E(theta) / E(0) = cos^4(theta), theta the image-space chief-ray angle with\n * tan(theta) = h / z for image height h and exit-pupil distance z (Greivenkamp, OPTI-502 course\n * notes, University of Arizona, sec. 11, free. Also see: Slater 1980 Photographic Systems for\n * Engineers; Kingslake, Lens Design Fundamentals, 2nd ed., sec. 6.5; Ray, Applied Photographic\n * Optics, 3rd ed., sec. 12). It is the small-pupil limit of the next model.\n * - finite-lambertian-pupil: exact irradiance from a uniform Lambertian disk of radius a at distance z\n * onto a parallel plane at lateral offset h (Foote 1915, Bull. Bureau of Standards 12:583):\n * E = (pi L / 2) [1 - (z^2 + h^2 - a^2) / sqrt((z^2 + h^2 + a^2)^2 - 4 a^2 h^2)],\n * normalised by its on-axis value pi L a^2 / (z^2 + a^2). For a -> 0 it tends to cos^4(theta).\n * Pupil aberration and pupil distortion are not modeled.\n *\n * Mechanical (optical) vignetting and the cat's-eye pupil:\n * - At field angle theta, each clipping aperture (a lens barrel or element rim, referred to the\n * entrance-pupil plane) appears as a circle of its declared radius. The circle is shifted by\n * shiftPerTanFieldMm * tan(theta) along the field azimuth.\n * - The open pupil is where the entrance-pupil circle overlaps every shifted circle. Off axis, the\n * bundle stays circular and centred on the chief ray, and an aperture passes only the part of it\n * that it covers (Greivenkamp, OPTI-502 course notes, University of Arizona, sec. 10, free. Also\n * see: Kingslake, Lens Design Fundamentals, sec. 6.4; Smith, Modern Optical Engineering, 4th ed.,\n * sec. 6.3).\n * - With one aperture, the area is the exact area of the two-circle overlap.\n * - With any number of apertures, the exact convex boundary is also returned, sampled as a polygon\n * in pupil-plane mm (+X right, +Y up). Use it as a polygon pupil for\n * calculatePolygonPupilFraunhoferPsf() or as a mask for createWavefrontPupilGrid().\n * The combined relative illumination is cos^4(theta) x open-area ratio. This assumes an object at\n * infinity, a stop imaged without pupil aberration, and an object-space field angle equal to the\n * chief-ray angle.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type NaturalVignettingModel =\n | \"cosine-fourth\"\n | \"finite-lambertian-pupil\";\n\nexport interface CalculateNaturalVignettingInput {\n model: NaturalVignettingModel;\n /** Image-plane point relative to the optical axis, mm. */\n imagePointMm: { x: number; y: number };\n /** Axial distance from the exit pupil to the image plane, mm. */\n exitPupilDistanceMm: number;\n /** Exit-pupil diameter, mm; required for finite-lambertian-pupil. */\n exitPupilDiameterMm?: number;\n}\n\nexport interface NaturalVignetting {\n model: NaturalVignettingModel;\n /** Image-space chief-ray angle, radians. */\n chiefRayAngleRadians: number;\n /** Relative illumination E / E(axis), in (0, 1]. */\n relativeIllumination: number;\n /** Positive exposure loss, stops. */\n attenuationStops: number;\n /** cos^4 of the chief-ray angle, reported for comparison. */\n cosineFourthFactor: number;\n}\n\nexport interface MechanicalVignettingAperture {\n /** Aperture radius referred to the entrance-pupil plane, mm. */\n radiusMm: number;\n /**\n * Signed shift of the aperture's projected centre per unit tan(field angle), in mm, along the\n * field azimuth. Positive means toward the field point.\n */\n shiftPerTanFieldMm: number;\n}\n\nexport interface CalculateMechanicalVignettingInput {\n /** Unclipped entrance-pupil diameter, mm. */\n entrancePupilDiameterMm: number;\n /** Object-space field angle from the optical axis, degrees, in [0, 90). */\n fieldAngleDegrees: number;\n /** Image-plane direction of the field point, degrees counter-clockwise from +X. */\n fieldAzimuthDegrees: number;\n /** One through eight clipping apertures. */\n apertures: readonly MechanicalVignettingAperture[];\n /** Boundary samples per circle for the polygon, from 16 to 4096 (default 256). */\n boundarySamplesPerCircle?: number;\n}\n\nexport interface MechanicalVignetting {\n fieldAngleDegrees: number;\n fieldAzimuthDegrees: number;\n entrancePupilAreaSquareMm: number;\n /** Open area in mm^2. Exact for a single aperture; the polygon area otherwise. */\n openAreaSquareMm: number;\n areaMethod: \"exact-circle-circle-intersection\" | \"boundary-polygon\";\n /** Open area / unclipped pupil area, in [0, 1]. */\n openAreaRatio: number;\n /** cos^4(field angle). */\n cosineFourthFactor: number;\n /** cos^4(theta) x openAreaRatio. */\n combinedRelativeIllumination: number;\n /** Counter-clockwise convex boundary of the open pupil (empty when fully vignetted), mm. */\n openPupilPolygonMm: readonly { x: number; y: number }[];\n /** Area centroid of the boundary polygon, mm (null when fully vignetted). */\n openPupilCentroidMm: { x: number; y: number } | null;\n}\n\nfunction requireFinite(name: string, value: unknown): number {\n if (typeof value !== \"number\" || !Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n return value;\n}\n\n/**\n * Natural (cos^4 or finite Lambertian pupil) relative illumination at one image point.\n */\nexport function calculateNaturalVignetting(\n input: CalculateNaturalVignettingInput\n): CalculationResult<NaturalVignetting> {\n requireInputKeys(input, \"calculateNaturalVignetting\", [\"model\", \"imagePointMm\", \"exitPupilDistanceMm\", \"exitPupilDiameterMm\"]);\n if (input.model !== \"cosine-fourth\" && input.model !== \"finite-lambertian-pupil\") {\n throw new InvalidConfigurationError(\n \"model must be cosine-fourth or finite-lambertian-pupil.\"\n );\n }\n if (typeof input.imagePointMm !== \"object\" || input.imagePointMm === null) {\n throw new InvalidConfigurationError(\"imagePointMm must be an object.\");\n }\n const h = Math.hypot(\n requireFinite(\"imagePointMm.x\", input.imagePointMm.x),\n requireFinite(\"imagePointMm.y\", input.imagePointMm.y)\n );\n requirePositiveFinite(\"exitPupilDistanceMm\", input.exitPupilDistanceMm);\n const z = input.exitPupilDistanceMm;\n const theta = Math.atan2(h, z);\n const cos2 = (z * z) / (z * z + h * h);\n const cosineFourthFactor = cos2 * cos2;\n\n let relativeIllumination: number;\n if (input.model === \"cosine-fourth\") {\n if (input.exitPupilDiameterMm !== undefined) {\n throw new InvalidScientificInputError(\n \"exitPupilDiameterMm is not used by the cosine-fourth model; omit it.\"\n );\n }\n relativeIllumination = cosineFourthFactor;\n } else {\n requirePositiveFinite(\"exitPupilDiameterMm\", input.exitPupilDiameterMm as number);\n const a = (input.exitPupilDiameterMm as number) / 2;\n const z2 = z * z;\n const h2 = h * h;\n const a2 = a * a;\n // Bracket term written as 2 a^2 (...) / (sqrt + ...) to avoid cancellation for small pupils:\n // 1 - P / Q = (Q - P) / Q with Q - P = (Q^2 - P^2) / (Q + P) = 4 a^2 z^2 / (Q + P).\n const p = z2 + h2 - a2;\n const q = Math.sqrt((z2 + h2 + a2) ** 2 - 4 * a2 * h2);\n const offAxis = (4 * a2 * z2) / (q * (q + p)) / 2;\n const onAxis = a2 / (z2 + a2);\n relativeIllumination = offAxis / onAxis;\n }\n if (!(relativeIllumination > 0) || !Number.isFinite(relativeIllumination)) {\n throw new InvalidScientificInputError(\n \"Natural vignetting relative illumination must remain finite and positive.\"\n );\n }\n relativeIllumination = Math.min(1, relativeIllumination);\n\n return calculatedResult(\n {\n model: input.model,\n chiefRayAngleRadians: theta,\n relativeIllumination,\n attenuationStops: relativeIllumination === 1 ? 0 : -Math.log2(relativeIllumination),\n cosineFourthFactor\n },\n input.model === \"cosine-fourth\"\n ? \"cosine-fourth-natural-vignetting\"\n : \"finite-lambertian-pupil-natural-vignetting\",\n \"1.0.0\",\n [\n input.model === \"cosine-fourth\"\n ? \"Relative illumination cos^4 of the image-space chief-ray angle from the exit-pupil centre\"\n : \"Exact irradiance from a uniform Lambertian circular exit pupil onto a parallel image plane, normalised on axis\",\n \"Aberration-free pupil: pupil aberration, pupil distortion and mechanical clipping are not modeled\",\n \"Throughput only; apply at most one field-throughput model, in scene-linear signal\"\n ]\n );\n}\n\n/** Exact area of the intersection of two circles. */\nfunction circleIntersectionArea(r1: number, r2: number, d: number): number {\n if (d >= r1 + r2) return 0;\n if (d <= Math.abs(r1 - r2)) return Math.PI * Math.min(r1, r2) ** 2;\n const clamp = (v: number): number => Math.max(-1, Math.min(1, v));\n const a1 = Math.acos(clamp((d * d + r1 * r1 - r2 * r2) / (2 * d * r1)));\n const a2 = Math.acos(clamp((d * d + r2 * r2 - r1 * r1) / (2 * d * r2)));\n const k = Math.sqrt(\n Math.max(0, (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2))\n );\n return r1 * r1 * a1 + r2 * r2 * a2 - k / 2;\n}\n\ninterface Circle {\n x: number;\n y: number;\n r: number;\n}\n\nfunction insideAll(\n point: { x: number; y: number },\n circles: readonly Circle[],\n skip: number\n): boolean {\n return circles.every(\n (circle, index) =>\n index === skip ||\n Math.hypot(point.x - circle.x, point.y - circle.y) <= circle.r * (1 + 1e-12)\n );\n}\n\n/** Convex boundary of the intersection of circles, sampled with vertices on the true boundary. */\nfunction intersectionPolygon(\n circles: readonly Circle[],\n samples: number\n): { x: number; y: number }[] {\n const points: { x: number; y: number }[] = [];\n circles.forEach((circle, index) => {\n for (let k = 0; k < samples; k += 1) {\n const angle = (2 * Math.PI * k) / samples;\n const point = {\n x: circle.x + circle.r * Math.cos(angle),\n y: circle.y + circle.r * Math.sin(angle)\n };\n if (insideAll(point, circles, index)) points.push(point);\n }\n });\n // Pairwise boundary intersection points (the cusps of the cat's eye).\n for (let i = 0; i < circles.length; i += 1) {\n for (let j = i + 1; j < circles.length; j += 1) {\n const a = circles[i]!;\n const b = circles[j]!;\n const d = Math.hypot(b.x - a.x, b.y - a.y);\n if (d === 0 || d >= a.r + b.r || d <= Math.abs(a.r - b.r)) continue;\n const along = (d * d + a.r * a.r - b.r * b.r) / (2 * d);\n const half = Math.sqrt(Math.max(0, a.r * a.r - along * along));\n const ux = (b.x - a.x) / d;\n const uy = (b.y - a.y) / d;\n for (const sign of [1, -1]) {\n const point = {\n x: a.x + along * ux - sign * half * uy,\n y: a.y + along * uy + sign * half * ux\n };\n if (circles.every((c) => Math.hypot(point.x - c.x, point.y - c.y) <= c.r * (1 + 1e-9))) {\n points.push(point);\n }\n }\n }\n }\n if (points.length < 3) return [];\n const cx = points.reduce((s, p) => s + p.x, 0) / points.length;\n const cy = points.reduce((s, p) => s + p.y, 0) / points.length;\n points.sort((p, q) => Math.atan2(p.y - cy, p.x - cx) - Math.atan2(q.y - cy, q.x - cx));\n // Drop coincident neighbours.\n const unique = points.filter((p, index) => {\n const prev = points[(index + points.length - 1) % points.length]!;\n return index === 0 || Math.hypot(p.x - prev.x, p.y - prev.y) > 1e-12 * (1 + Math.abs(p.x) + Math.abs(p.y));\n });\n return unique.length >= 3 ? unique : [];\n}\n\nfunction polygonAreaAndCentroid(\n polygon: readonly { x: number; y: number }[]\n): { area: number; centroid: { x: number; y: number } } {\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < polygon.length; i += 1) {\n const a = polygon[i]!;\n const b = polygon[(i + 1) % polygon.length]!;\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n return {\n area: twiceArea / 2,\n centroid: { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) }\n };\n}\n\n/**\n * Mechanical vignetting at one field angle: the open (cat's-eye) pupil from intersecting the\n * entrance pupil with displaced clipping apertures, its area ratio, and cos^4 x area ratio.\n */\nexport function calculateMechanicalVignetting(\n input: CalculateMechanicalVignettingInput\n): CalculationResult<MechanicalVignetting> {\n requireInputKeys(input, \"calculateMechanicalVignetting\", [\"entrancePupilDiameterMm\", \"fieldAngleDegrees\", \"fieldAzimuthDegrees\", \"apertures\", \"boundarySamplesPerCircle\"]);\n requirePositiveFinite(\"entrancePupilDiameterMm\", input.entrancePupilDiameterMm);\n const fieldAngleDegrees = requireFinite(\"fieldAngleDegrees\", input.fieldAngleDegrees);\n if (fieldAngleDegrees < 0 || fieldAngleDegrees >= 90) {\n throw new InvalidScientificInputError(\"fieldAngleDegrees must lie in [0, 90).\");\n }\n const fieldAzimuthDegrees = requireFinite(\"fieldAzimuthDegrees\", input.fieldAzimuthDegrees);\n if (!Array.isArray(input.apertures) || input.apertures.length < 1 || input.apertures.length > 8) {\n throw new InvalidScientificInputError(\"apertures must contain 1 through 8 entries.\");\n }\n const samples = input.boundarySamplesPerCircle ?? 256;\n if (!Number.isSafeInteger(samples) || samples < 16 || samples > 4096) {\n throw new InvalidScientificInputError(\n \"boundarySamplesPerCircle must be an integer from 16 through 4096.\"\n );\n }\n const theta = (fieldAngleDegrees * Math.PI) / 180;\n const azimuth = (fieldAzimuthDegrees * Math.PI) / 180;\n const tanTheta = Math.tan(theta);\n const pupilRadius = input.entrancePupilDiameterMm / 2;\n const circles: Circle[] = [{ x: 0, y: 0, r: pupilRadius }];\n input.apertures.forEach((aperture, index) => {\n if (typeof aperture !== \"object\" || aperture === null) {\n throw new InvalidConfigurationError(`apertures[${index}] must be an object.`);\n }\n requirePositiveFinite(`apertures[${index}].radiusMm`, aperture.radiusMm);\n const shift =\n requireFinite(`apertures[${index}].shiftPerTanFieldMm`, aperture.shiftPerTanFieldMm) *\n tanTheta;\n circles.push({\n x: shift * Math.cos(azimuth),\n y: shift * Math.sin(azimuth),\n r: aperture.radiusMm\n });\n });\n\n const pupilArea = Math.PI * pupilRadius * pupilRadius;\n const polygon = intersectionPolygon(circles, samples);\n const geometry = polygon.length >= 3 ? polygonAreaAndCentroid(polygon) : undefined;\n let openArea: number;\n let areaMethod: MechanicalVignetting[\"areaMethod\"];\n if (circles.length === 2) {\n const other = circles[1]!;\n openArea = circleIntersectionArea(pupilRadius, other.r, Math.hypot(other.x, other.y));\n areaMethod = \"exact-circle-circle-intersection\";\n } else {\n openArea = geometry?.area ?? 0;\n areaMethod = \"boundary-polygon\";\n }\n const openAreaRatio = Math.min(1, Math.max(0, openArea / pupilArea));\n const cos2 = Math.cos(theta) ** 2;\n const cosineFourthFactor = cos2 * cos2;\n\n return approximationResult(\n {\n fieldAngleDegrees,\n fieldAzimuthDegrees,\n entrancePupilAreaSquareMm: pupilArea,\n openAreaSquareMm: openArea,\n areaMethod,\n openAreaRatio,\n cosineFourthFactor,\n combinedRelativeIllumination: cosineFourthFactor * openAreaRatio,\n openPupilPolygonMm: polygon,\n openPupilCentroidMm: geometry?.centroid ?? null\n },\n \"cats-eye-mechanical-vignetting\",\n \"1.0.0\",\n [\n \"Clipping apertures are circles referred to the entrance-pupil plane, displaced linearly with tan(field angle) along the field azimuth\",\n \"Open pupil = intersection of the entrance pupil with every displaced aperture; boundary vertices lie exactly on the circles\",\n \"Combined relative illumination cos^4(theta) x open-area ratio assumes an object at infinity and no pupil aberration\",\n \"Pupil coordinates are metric and aligned with the image plane (+X right, +Y up)\",\n \"Geometry is caller-declared; no named-lens barrel data is implied\"\n ]\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pupil-vignetting.js","sourceRoot":"","sources":["../../src/optics/pupil-vignetting.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Physical vignetting models derived from pupil geometry. They are separate from the\n * calculateIlluminationVignetting() polynomial, whose coefficients the caller supplies.\n *\n * Apply at most one field-throughput model to a signal. Do not stack these results on top of a\n * supplied field-throughput profile.\n *\n * Natural vignetting (relative illumination of an aberration-free system with a uniform Lambertian\n * exit pupil):\n * - cosine-fourth: E(theta) / E(0) = cos^4(theta), theta the image-space chief-ray angle with\n * tan(theta) = h / z for image height h and exit-pupil distance z (Greivenkamp, OPTI-502 course\n * notes, University of Arizona, sec. 11, free. Also see: Slater 1980 Photographic Systems for\n * Engineers; Kingslake, Lens Design Fundamentals, 2nd ed., sec. 6.5; Ray, Applied Photographic\n * Optics, 3rd ed., sec. 12). It is the small-pupil limit of the next model.\n * - finite-lambertian-pupil: exact irradiance from a uniform Lambertian disk of radius a at distance z\n * onto a parallel plane at lateral offset h (Foote 1915, Bull. Bureau of Standards 12:583):\n * E = (pi L / 2) [1 - (z^2 + h^2 - a^2) / sqrt((z^2 + h^2 + a^2)^2 - 4 a^2 h^2)],\n * normalised by its on-axis value pi L a^2 / (z^2 + a^2). For a -> 0 it tends to cos^4(theta).\n * Pupil aberration and pupil distortion are not modeled.\n *\n * Mechanical (optical) vignetting and the cat's-eye pupil:\n * - At field angle theta, each clipping aperture (a lens barrel or element rim, referred to the\n * entrance-pupil plane) appears as a circle of its declared radius. The circle is shifted by\n * shiftPerTanFieldMm * tan(theta) along the field azimuth.\n * - The open pupil is where the entrance-pupil circle overlaps every shifted circle. Off axis, the\n * bundle stays circular and centred on the chief ray, and an aperture passes only the part of it\n * that it covers (Greivenkamp, OPTI-502 course notes, University of Arizona, sec. 10, free. Also\n * see: Kingslake, Lens Design Fundamentals, sec. 6.4; Smith, Modern Optical Engineering, 4th ed.,\n * sec. 6.3).\n * - With one aperture, the area is the exact area of the two-circle overlap.\n * - With any number of apertures, the exact convex boundary is also returned, sampled as a polygon\n * in pupil-plane mm (+X right, +Y up). Use it as a polygon pupil for\n * calculatePolygonPupilFraunhoferPsf() or as a mask for createWavefrontPupilGrid().\n * The combined relative illumination is cos^4(theta) x open-area ratio. This assumes an object at\n * infinity, a stop imaged without pupil aberration, and an object-space field angle equal to the\n * chief-ray angle.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n calculatedResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { requireInputKeys, requireNamedFinite as requireFinite } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport type NaturalVignettingModel =\n | \"cosine-fourth\"\n | \"finite-lambertian-pupil\";\n\nexport interface CalculateNaturalVignettingInput {\n model: NaturalVignettingModel;\n /** Image-plane point relative to the optical axis, mm. */\n imagePointMm: { x: number; y: number };\n /** Axial distance from the exit pupil to the image plane, mm. */\n exitPupilDistanceMm: number;\n /** Exit-pupil diameter, mm; required for finite-lambertian-pupil. */\n exitPupilDiameterMm?: number;\n}\n\nexport interface NaturalVignetting {\n model: NaturalVignettingModel;\n /** Image-space chief-ray angle, radians. */\n chiefRayAngleRadians: number;\n /** Relative illumination E / E(axis), in (0, 1]. */\n relativeIllumination: number;\n /** Positive exposure loss, stops. */\n attenuationStops: number;\n /** cos^4 of the chief-ray angle, reported for comparison. */\n cosineFourthFactor: number;\n}\n\nexport interface MechanicalVignettingAperture {\n /** Aperture radius referred to the entrance-pupil plane, mm. */\n radiusMm: number;\n /**\n * Signed shift of the aperture's projected centre per unit tan(field angle), in mm, along the\n * field azimuth. Positive means toward the field point.\n */\n shiftPerTanFieldMm: number;\n}\n\nexport interface CalculateMechanicalVignettingInput {\n /** Unclipped entrance-pupil diameter, mm. */\n entrancePupilDiameterMm: number;\n /** Object-space field angle from the optical axis, degrees, in [0, 90). */\n fieldAngleDegrees: number;\n /** Image-plane direction of the field point, degrees counter-clockwise from +X. */\n fieldAzimuthDegrees: number;\n /** One through eight clipping apertures. */\n apertures: readonly MechanicalVignettingAperture[];\n /** Boundary samples per circle for the polygon, from 16 to 4096 (default 256). */\n boundarySamplesPerCircle?: number;\n}\n\nexport interface MechanicalVignetting {\n fieldAngleDegrees: number;\n fieldAzimuthDegrees: number;\n entrancePupilAreaSquareMm: number;\n /** Open area in mm^2. Exact for a single aperture; the polygon area otherwise. */\n openAreaSquareMm: number;\n areaMethod: \"exact-circle-circle-intersection\" | \"boundary-polygon\";\n /** Open area / unclipped pupil area, in [0, 1]. */\n openAreaRatio: number;\n /** cos^4(field angle). */\n cosineFourthFactor: number;\n /** cos^4(theta) x openAreaRatio. */\n combinedRelativeIllumination: number;\n /** Counter-clockwise convex boundary of the open pupil (empty when fully vignetted), mm. */\n openPupilPolygonMm: readonly { x: number; y: number }[];\n /** Area centroid of the boundary polygon, mm (null when fully vignetted). */\n openPupilCentroidMm: { x: number; y: number } | null;\n}\n\n/**\n * Natural (cos^4 or finite Lambertian pupil) relative illumination at one image point.\n */\nexport function calculateNaturalVignetting(\n input: CalculateNaturalVignettingInput\n): CalculationResult<NaturalVignetting> {\n requireInputKeys(input, \"calculateNaturalVignetting\", [\"model\", \"imagePointMm\", \"exitPupilDistanceMm\", \"exitPupilDiameterMm\"]);\n if (input.model !== \"cosine-fourth\" && input.model !== \"finite-lambertian-pupil\") {\n throw new InvalidConfigurationError(\n \"model must be cosine-fourth or finite-lambertian-pupil.\"\n );\n }\n if (typeof input.imagePointMm !== \"object\" || input.imagePointMm === null) {\n throw new InvalidConfigurationError(\"imagePointMm must be an object.\");\n }\n const h = Math.hypot(\n requireFinite(\"imagePointMm.x\", input.imagePointMm.x),\n requireFinite(\"imagePointMm.y\", input.imagePointMm.y)\n );\n requirePositiveFinite(\"exitPupilDistanceMm\", input.exitPupilDistanceMm);\n const z = input.exitPupilDistanceMm;\n const theta = Math.atan2(h, z);\n const cos2 = (z * z) / (z * z + h * h);\n const cosineFourthFactor = cos2 * cos2;\n\n let relativeIllumination: number;\n if (input.model === \"cosine-fourth\") {\n if (input.exitPupilDiameterMm !== undefined) {\n throw new InvalidScientificInputError(\n \"exitPupilDiameterMm is not used by the cosine-fourth model; omit it.\"\n );\n }\n relativeIllumination = cosineFourthFactor;\n } else {\n requirePositiveFinite(\"exitPupilDiameterMm\", input.exitPupilDiameterMm as number);\n const a = (input.exitPupilDiameterMm as number) / 2;\n const z2 = z * z;\n const h2 = h * h;\n const a2 = a * a;\n // Bracket term written as 2 a^2 (...) / (sqrt + ...) to avoid cancellation for small pupils:\n // 1 - P / Q = (Q - P) / Q with Q - P = (Q^2 - P^2) / (Q + P) = 4 a^2 z^2 / (Q + P).\n const p = z2 + h2 - a2;\n const q = Math.sqrt((z2 + h2 + a2) ** 2 - 4 * a2 * h2);\n const offAxis = (4 * a2 * z2) / (q * (q + p)) / 2;\n const onAxis = a2 / (z2 + a2);\n relativeIllumination = offAxis / onAxis;\n }\n if (!(relativeIllumination > 0) || !Number.isFinite(relativeIllumination)) {\n throw new InvalidScientificInputError(\n \"Natural vignetting relative illumination must remain finite and positive.\"\n );\n }\n relativeIllumination = Math.min(1, relativeIllumination);\n\n return calculatedResult(\n {\n model: input.model,\n chiefRayAngleRadians: theta,\n relativeIllumination,\n attenuationStops: relativeIllumination === 1 ? 0 : -Math.log2(relativeIllumination),\n cosineFourthFactor\n },\n input.model === \"cosine-fourth\"\n ? \"cosine-fourth-natural-vignetting\"\n : \"finite-lambertian-pupil-natural-vignetting\",\n \"1.0.0\",\n [\n input.model === \"cosine-fourth\"\n ? \"Relative illumination cos^4 of the image-space chief-ray angle from the exit-pupil centre\"\n : \"Exact irradiance from a uniform Lambertian circular exit pupil onto a parallel image plane, normalised on axis\",\n \"Aberration-free pupil: pupil aberration, pupil distortion and mechanical clipping are not modeled\",\n \"Throughput only; apply at most one field-throughput model, in scene-linear signal\"\n ]\n );\n}\n\n/** Exact area of the intersection of two circles. */\nfunction circleIntersectionArea(r1: number, r2: number, d: number): number {\n if (d >= r1 + r2) return 0;\n if (d <= Math.abs(r1 - r2)) return Math.PI * Math.min(r1, r2) ** 2;\n const clamp = (v: number): number => Math.max(-1, Math.min(1, v));\n const a1 = Math.acos(clamp((d * d + r1 * r1 - r2 * r2) / (2 * d * r1)));\n const a2 = Math.acos(clamp((d * d + r2 * r2 - r1 * r1) / (2 * d * r2)));\n const k = Math.sqrt(\n Math.max(0, (-d + r1 + r2) * (d + r1 - r2) * (d - r1 + r2) * (d + r1 + r2))\n );\n return r1 * r1 * a1 + r2 * r2 * a2 - k / 2;\n}\n\ninterface Circle {\n x: number;\n y: number;\n r: number;\n}\n\nfunction insideAll(\n point: { x: number; y: number },\n circles: readonly Circle[],\n skip: number\n): boolean {\n return circles.every(\n (circle, index) =>\n index === skip ||\n Math.hypot(point.x - circle.x, point.y - circle.y) <= circle.r * (1 + 1e-12)\n );\n}\n\n/** Convex boundary of the intersection of circles, sampled with vertices on the true boundary. */\nfunction intersectionPolygon(\n circles: readonly Circle[],\n samples: number\n): { x: number; y: number }[] {\n const points: { x: number; y: number }[] = [];\n circles.forEach((circle, index) => {\n for (let k = 0; k < samples; k += 1) {\n const angle = (2 * Math.PI * k) / samples;\n const point = {\n x: circle.x + circle.r * Math.cos(angle),\n y: circle.y + circle.r * Math.sin(angle)\n };\n if (insideAll(point, circles, index)) points.push(point);\n }\n });\n // Pairwise boundary intersection points (the cusps of the cat's eye).\n for (let i = 0; i < circles.length; i += 1) {\n for (let j = i + 1; j < circles.length; j += 1) {\n const a = circles[i]!;\n const b = circles[j]!;\n const d = Math.hypot(b.x - a.x, b.y - a.y);\n if (d === 0 || d >= a.r + b.r || d <= Math.abs(a.r - b.r)) continue;\n const along = (d * d + a.r * a.r - b.r * b.r) / (2 * d);\n const half = Math.sqrt(Math.max(0, a.r * a.r - along * along));\n const ux = (b.x - a.x) / d;\n const uy = (b.y - a.y) / d;\n for (const sign of [1, -1]) {\n const point = {\n x: a.x + along * ux - sign * half * uy,\n y: a.y + along * uy + sign * half * ux\n };\n if (circles.every((c) => Math.hypot(point.x - c.x, point.y - c.y) <= c.r * (1 + 1e-9))) {\n points.push(point);\n }\n }\n }\n }\n if (points.length < 3) return [];\n const cx = points.reduce((s, p) => s + p.x, 0) / points.length;\n const cy = points.reduce((s, p) => s + p.y, 0) / points.length;\n points.sort((p, q) => Math.atan2(p.y - cy, p.x - cx) - Math.atan2(q.y - cy, q.x - cx));\n // Drop coincident neighbours.\n const unique = points.filter((p, index) => {\n const prev = points[(index + points.length - 1) % points.length]!;\n return index === 0 || Math.hypot(p.x - prev.x, p.y - prev.y) > 1e-12 * (1 + Math.abs(p.x) + Math.abs(p.y));\n });\n return unique.length >= 3 ? unique : [];\n}\n\nfunction polygonAreaAndCentroid(\n polygon: readonly { x: number; y: number }[]\n): { area: number; centroid: { x: number; y: number } } {\n let twiceArea = 0;\n let cx = 0;\n let cy = 0;\n for (let i = 0; i < polygon.length; i += 1) {\n const a = polygon[i]!;\n const b = polygon[(i + 1) % polygon.length]!;\n const cross = a.x * b.y - b.x * a.y;\n twiceArea += cross;\n cx += (a.x + b.x) * cross;\n cy += (a.y + b.y) * cross;\n }\n return {\n area: twiceArea / 2,\n centroid: { x: cx / (3 * twiceArea), y: cy / (3 * twiceArea) }\n };\n}\n\n/**\n * Mechanical vignetting at one field angle: the open (cat's-eye) pupil from intersecting the\n * entrance pupil with displaced clipping apertures, its area ratio, and cos^4 x area ratio.\n */\nexport function calculateMechanicalVignetting(\n input: CalculateMechanicalVignettingInput\n): CalculationResult<MechanicalVignetting> {\n requireInputKeys(input, \"calculateMechanicalVignetting\", [\"entrancePupilDiameterMm\", \"fieldAngleDegrees\", \"fieldAzimuthDegrees\", \"apertures\", \"boundarySamplesPerCircle\"]);\n requirePositiveFinite(\"entrancePupilDiameterMm\", input.entrancePupilDiameterMm);\n const fieldAngleDegrees = requireFinite(\"fieldAngleDegrees\", input.fieldAngleDegrees);\n if (fieldAngleDegrees < 0 || fieldAngleDegrees >= 90) {\n throw new InvalidScientificInputError(\"fieldAngleDegrees must lie in [0, 90).\");\n }\n const fieldAzimuthDegrees = requireFinite(\"fieldAzimuthDegrees\", input.fieldAzimuthDegrees);\n if (!Array.isArray(input.apertures) || input.apertures.length < 1 || input.apertures.length > 8) {\n throw new InvalidScientificInputError(\"apertures must contain 1 through 8 entries.\");\n }\n const samples = input.boundarySamplesPerCircle ?? 256;\n if (!Number.isSafeInteger(samples) || samples < 16 || samples > 4096) {\n throw new InvalidScientificInputError(\n \"boundarySamplesPerCircle must be an integer from 16 through 4096.\"\n );\n }\n const theta = (fieldAngleDegrees * Math.PI) / 180;\n const azimuth = (fieldAzimuthDegrees * Math.PI) / 180;\n const tanTheta = Math.tan(theta);\n const pupilRadius = input.entrancePupilDiameterMm / 2;\n const circles: Circle[] = [{ x: 0, y: 0, r: pupilRadius }];\n input.apertures.forEach((aperture, index) => {\n if (typeof aperture !== \"object\" || aperture === null) {\n throw new InvalidConfigurationError(`apertures[${index}] must be an object.`);\n }\n requirePositiveFinite(`apertures[${index}].radiusMm`, aperture.radiusMm);\n const shift =\n requireFinite(`apertures[${index}].shiftPerTanFieldMm`, aperture.shiftPerTanFieldMm) *\n tanTheta;\n circles.push({\n x: shift * Math.cos(azimuth),\n y: shift * Math.sin(azimuth),\n r: aperture.radiusMm\n });\n });\n\n const pupilArea = Math.PI * pupilRadius * pupilRadius;\n const polygon = intersectionPolygon(circles, samples);\n const geometry = polygon.length >= 3 ? polygonAreaAndCentroid(polygon) : undefined;\n let openArea: number;\n let areaMethod: MechanicalVignetting[\"areaMethod\"];\n if (circles.length === 2) {\n const other = circles[1]!;\n openArea = circleIntersectionArea(pupilRadius, other.r, Math.hypot(other.x, other.y));\n areaMethod = \"exact-circle-circle-intersection\";\n } else {\n openArea = geometry?.area ?? 0;\n areaMethod = \"boundary-polygon\";\n }\n const openAreaRatio = Math.min(1, Math.max(0, openArea / pupilArea));\n const cos2 = Math.cos(theta) ** 2;\n const cosineFourthFactor = cos2 * cos2;\n\n return approximationResult(\n {\n fieldAngleDegrees,\n fieldAzimuthDegrees,\n entrancePupilAreaSquareMm: pupilArea,\n openAreaSquareMm: openArea,\n areaMethod,\n openAreaRatio,\n cosineFourthFactor,\n combinedRelativeIllumination: cosineFourthFactor * openAreaRatio,\n openPupilPolygonMm: polygon,\n openPupilCentroidMm: geometry?.centroid ?? null\n },\n \"cats-eye-mechanical-vignetting\",\n \"1.0.0\",\n [\n \"Clipping apertures are circles referred to the entrance-pupil plane, displaced linearly with tan(field angle) along the field azimuth\",\n \"Open pupil = intersection of the entrance pupil with every displaced aperture; boundary vertices lie exactly on the circles\",\n \"Combined relative illumination cos^4(theta) x open-area ratio assumes an object at infinity and no pupil aberration\",\n \"Pupil coordinates are metric and aligned with the image plane (+X right, +Y up)\",\n \"Geometry is caller-declared; no named-lens barrel data is implied\"\n ]\n );\n}\n"]}
|
|
@@ -13,15 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { approximationResult } from "../core/calculation-result.js";
|
|
15
15
|
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
16
|
-
import { requireInputObject, requireInputKeys } from "../core/record-validation.js";
|
|
16
|
+
import { requireInputObject, requireInputKeys, assertNamedFinite as requireFinite } from "../core/record-validation.js";
|
|
17
17
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
18
18
|
const INVERSE_BISECTION_ITERATIONS = 80;
|
|
19
19
|
const INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS = 16;
|
|
20
|
-
function requireFinite(name, value) {
|
|
21
|
-
if (!Number.isFinite(value)) {
|
|
22
|
-
throw new InvalidScientificInputError(`${name} must be finite.`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
20
|
function inverseMappedRadiusTolerance(distortedNormalizedRadius, maximumMappedNormalizedRadius) {
|
|
26
21
|
return (Number.EPSILON *
|
|
27
22
|
INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS *
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radial-distortion.js","sourceRoot":"","sources":["../../src/optics/radial-distortion.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic radial lens distortion (barrel and pincushion).\n *\n * The mapping is rotationally symmetric about the optical axis:\n * p_distorted = p_ideal * (1 + k1 r² + k2 r⁴ + k3 r⁶)\n * where r is normalized by the physical reference radius the caller declares.\n *\n * A profile is accepted only if the radial mapping is strictly monotonic over its declared operating\n * radius. That makes the inverse mapping (destination-to-source sampling) well defined.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { requireInputObject, requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport interface RadialDistortionCoefficients {\n /** Dimensionless r² coefficient. */\n k1: number;\n /** Dimensionless r⁴ coefficient. */\n k2: number;\n /** Dimensionless r⁶ coefficient. */\n k3: number;\n}\n\nexport interface RadialDistortionProfile {\n /**\n * Physical image-plane radius used to normalize field position, in mm.\n * Coefficients are meaningful only with this declared normalization.\n */\n normalizationRadiusMm: number;\n /**\n * Largest undistorted normalized radius for which this profile is declared\n * valid. Within it the mapping must stay one-to-one and invertible.\n */\n maximumNormalizedRadius: number;\n coefficients: RadialDistortionCoefficients;\n}\n\nexport interface LensFieldPointMm {\n /** Image-plane X coordinate relative to the optical axis, in mm. */\n x: number;\n /** Image-plane Y coordinate relative to the optical axis, in mm. */\n y: number;\n}\n\nexport interface CalculateRadialDistortionMappingInput {\n /** Ideal/undistorted image-plane point. */\n imagePointMm: LensFieldPointMm;\n profile: RadialDistortionProfile;\n}\n\nexport interface CalculateInverseRadialDistortionMappingInput {\n /** Distorted image-plane point to inverse-map back to ideal coordinates. */\n distortedImagePointMm: LensFieldPointMm;\n profile: RadialDistortionProfile;\n}\n\nexport interface CalculateInverseRadialDistortionMappingsInput {\n /** Distorted image-plane destinations to inverse-map in one validated batch. */\n distortedImagePointsMm: readonly LensFieldPointMm[];\n profile: RadialDistortionProfile;\n}\n\nexport interface RadialDistortionMapping {\n direction: \"undistorted-to-distorted\";\n sourceImagePointMm: LensFieldPointMm;\n mappedImagePointMm: LensFieldPointMm;\n sourceNormalizedRadius: number;\n mappedNormalizedRadius: number;\n radialScale: number;\n deltaMm: {\n x: number;\n y: number;\n distance: number;\n };\n profileMinimumRadialDerivative: number;\n}\n\nexport interface InverseRadialDistortionMapping {\n direction: \"distorted-to-undistorted\";\n distortedImagePointMm: LensFieldPointMm;\n sourceImagePointMm: LensFieldPointMm;\n distortedNormalizedRadius: number;\n sourceNormalizedRadius: number;\n radialScaleAtSource: number;\n deltaMm: {\n x: number;\n y: number;\n distance: number;\n };\n profileMinimumRadialDerivative: number;\n}\n\nexport interface InverseRadialDistortionMappings {\n direction: \"distorted-to-undistorted-batch\";\n mappings: readonly InverseRadialDistortionMapping[];\n pointCount: number;\n profileMinimumRadialDerivative: number;\n}\n\n/** @internal Shared by the composite lens-field batch evaluators. Not exported from the package root. */\nexport interface ValidatedRadialProfile {\n normalizationRadiusMm: number;\n maximumNormalizedRadius: number;\n coefficients: RadialDistortionCoefficients;\n minimumRadialDerivative: number;\n maximumMappedNormalizedRadius: number;\n}\n\nconst INVERSE_BISECTION_ITERATIONS = 80;\nconst INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS = 16;\n\nfunction requireFinite(name: string, value: number): void {\n if (!Number.isFinite(value)) {\n throw new InvalidScientificInputError(`${name} must be finite.`);\n }\n}\n\nfunction inverseMappedRadiusTolerance(\n distortedNormalizedRadius: number,\n maximumMappedNormalizedRadius: number\n): number {\n return (\n Number.EPSILON *\n INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS *\n Math.max(\n 1,\n Math.abs(distortedNormalizedRadius),\n Math.abs(maximumMappedNormalizedRadius)\n )\n );\n}\n\nfunction radialScale(\n normalizedRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n const r2 = normalizedRadius * normalizedRadius;\n const r4 = r2 * r2;\n const r6 = r4 * r2;\n return (\n 1 +\n coefficients.k1 * r2 +\n coefficients.k2 * r4 +\n coefficients.k3 * r6\n );\n}\n\nfunction mappedNormalizedRadius(\n normalizedRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n return normalizedRadius * radialScale(normalizedRadius, coefficients);\n}\n\nfunction radialDerivativeFromSquaredRadius(\n squaredRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n return (\n 1 +\n 3 * coefficients.k1 * squaredRadius +\n 5 * coefficients.k2 * squaredRadius * squaredRadius +\n 7 *\n coefficients.k3 *\n squaredRadius *\n squaredRadius *\n squaredRadius\n );\n}\n\nfunction derivativeCriticalSquaredRadii(\n coefficients: RadialDistortionCoefficients\n): number[] {\n // d/dr² of the radial derivative:\n // 3 k1 + 10 k2 s + 21 k3 s² = 0, where s = r².\n //\n // Scale the quadratic before solving, so finite caller coefficients cannot\n // overflow the discriminant. Use the numerically stable q-form roots. Then a\n // tiny quadratic term cannot erase the smaller physical root through\n // cancellation and let a non-monotonic profile pass by accident.\n const a = 21 * coefficients.k3;\n const b = 10 * coefficients.k2;\n const c = 3 * coefficients.k1;\n const coefficientScale = Math.max(\n Math.abs(a),\n Math.abs(b),\n Math.abs(c)\n );\n\n if (coefficientScale === 0) {\n return [];\n }\n\n const scaledA = a / coefficientScale;\n const scaledB = b / coefficientScale;\n const scaledC = c / coefficientScale;\n\n if (scaledA === 0) {\n return scaledB === 0 ? [] : [-scaledC / scaledB];\n }\n\n const squaredB = scaledB * scaledB;\n const fourAC = 4 * scaledA * scaledC;\n const discriminant = squaredB - fourAC;\n const discriminantTolerance =\n Number.EPSILON *\n 16 *\n Math.max(1, Math.abs(squaredB), Math.abs(fourAC));\n\n if (discriminant < -discriminantTolerance) {\n return [];\n }\n\n const root = Math.sqrt(Math.max(0, discriminant));\n if (root === 0) {\n return [-scaledB / (2 * scaledA)];\n }\n\n // root > 0 here, so q is never zero: for scaledB >= 0 the sum scaledB + root is positive and for\n // scaledB < 0 the difference scaledB - root is negative. (A former q === 0 branch was unreachable.)\n const q =\n -0.5 *\n (scaledB + (scaledB >= 0 ? root : -root));\n\n return [q / scaledA, scaledC / q];\n}\n\n/** @internal Validates and copies one profile for repeated use inside the package. */\nexport function validateRadialDistortionProfile(\n profile: RadialDistortionProfile\n): ValidatedRadialProfile {\n requireInputObject(profile, \"profile input\");\n requirePositiveFinite(\n \"profile.normalizationRadiusMm\",\n profile.normalizationRadiusMm\n );\n requirePositiveFinite(\n \"profile.maximumNormalizedRadius\",\n profile.maximumNormalizedRadius\n );\n requireFinite(\"profile.coefficients.k1\", profile.coefficients.k1);\n requireFinite(\"profile.coefficients.k2\", profile.coefficients.k2);\n requireFinite(\"profile.coefficients.k3\", profile.coefficients.k3);\n\n const maximumSquaredRadius =\n profile.maximumNormalizedRadius * profile.maximumNormalizedRadius;\n if (!Number.isFinite(maximumSquaredRadius)) {\n throw new InvalidScientificInputError(\n \"profile.maximumNormalizedRadius is too large to evaluate safely.\"\n );\n }\n\n const candidates = [\n 0,\n maximumSquaredRadius,\n ...derivativeCriticalSquaredRadii(profile.coefficients).filter(\n (value) => value > 0 && value < maximumSquaredRadius\n )\n ];\n const minimumRadialDerivative = Math.min(\n ...candidates.map((squaredRadius) =>\n radialDerivativeFromSquaredRadius(\n squaredRadius,\n profile.coefficients\n )\n )\n );\n\n if (\n !Number.isFinite(minimumRadialDerivative) ||\n minimumRadialDerivative <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Radial distortion profile must remain strictly monotonic over maximumNormalizedRadius so inverse mapping is single-valued.\"\n );\n }\n\n const maximumMappedRadius = mappedNormalizedRadius(\n profile.maximumNormalizedRadius,\n profile.coefficients\n );\n if (!Number.isFinite(maximumMappedRadius) || maximumMappedRadius <= 0) {\n throw new InvalidScientificInputError(\n \"Radial distortion profile produces an invalid mapped operating radius.\"\n );\n }\n\n return {\n normalizationRadiusMm: profile.normalizationRadiusMm,\n maximumNormalizedRadius: profile.maximumNormalizedRadius,\n coefficients: { ...profile.coefficients },\n minimumRadialDerivative,\n maximumMappedNormalizedRadius: maximumMappedRadius\n };\n}\n\nfunction requirePoint(name: string, point: LensFieldPointMm): void {\n if (typeof point !== \"object\" || point === null) {\n throw new InvalidConfigurationError(\n `${name} must be an object with finite numeric x and y coordinates.`\n );\n }\n if (\n typeof point.x !== \"number\" ||\n typeof point.y !== \"number\" ||\n !Number.isFinite(point.x) ||\n !Number.isFinite(point.y)\n ) {\n throw new InvalidScientificInputError(\n `${name} must contain finite numeric x and y coordinates.`\n );\n }\n}\n\nfunction mappingAssumptions(): readonly string[] {\n return [\n \"Generic rotationally symmetric radial polynomial field mapping\",\n \"Distortion is centered on the optical axis; decentering and tangential distortion are not modeled\",\n \"Normalized radius is image-plane radius divided by the caller-declared normalizationRadiusMm\",\n \"Radial scale is 1 + k1 r^2 + k2 r^4 + k3 r^6\",\n \"The declared operating radius is required to remain strictly monotonic so inverse mapping is single-valued\",\n \"Coefficients are generic caller inputs and do not represent a named lens unless separately calibrated with defensible provenance\"\n ];\n}\n\n/**\n * Applies a generic radial lens-distortion field mapping to one ideal\n * image-plane point.\n *\n * The mapping is rotationally symmetric about the optical axis:\n *\n * p_distorted = p_ideal * (1 + k1 r² + k2 r⁴ + k3 r⁶)\n *\n * where r is normalized by the caller-declared physical reference radius.\n *\n * A profile is accepted only if the radial mapping is strictly monotonic over\n * its declared operating radius. That makes the inverse mapping\n * (destination-to-source sampling) well defined.\n */\nexport function calculateRadialDistortionMapping(\n input: CalculateRadialDistortionMappingInput\n): CalculationResult<RadialDistortionMapping> {\n requireInputKeys(input, \"calculateRadialDistortionMapping\", [\"imagePointMm\", \"profile\"]);\n requirePoint(\"imagePointMm\", input.imagePointMm);\n const profile = validateRadialDistortionProfile(input.profile);\n\n const sourceRadiusMm = Math.hypot(\n input.imagePointMm.x,\n input.imagePointMm.y\n );\n const sourceNormalizedRadius =\n sourceRadiusMm / profile.normalizationRadiusMm;\n requireFinite(\"imagePointMm normalized radius\", sourceNormalizedRadius);\n\n if (sourceNormalizedRadius > profile.maximumNormalizedRadius) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the radial distortion profile maximumNormalizedRadius.\"\n );\n }\n\n const scale = radialScale(\n sourceNormalizedRadius,\n profile.coefficients\n );\n const mappedImagePointMm = {\n x: input.imagePointMm.x * scale,\n y: input.imagePointMm.y * scale\n };\n const mappedRadius = sourceNormalizedRadius * scale;\n const deltaX = mappedImagePointMm.x - input.imagePointMm.x;\n const deltaY = mappedImagePointMm.y - input.imagePointMm.y;\n\n return approximationResult(\n {\n direction: \"undistorted-to-distorted\",\n sourceImagePointMm: { ...input.imagePointMm },\n mappedImagePointMm,\n sourceNormalizedRadius,\n mappedNormalizedRadius: mappedRadius,\n radialScale: scale,\n deltaMm: {\n x: deltaX,\n y: deltaY,\n distance: Math.hypot(deltaX, deltaY)\n },\n profileMinimumRadialDerivative:\n profile.minimumRadialDerivative\n },\n \"generic-radial-lens-distortion\",\n \"1.0.0\",\n mappingAssumptions()\n );\n}\n\n/**\n * Inverse-maps one distorted image-plane point to its ideal (undistorted)\n * source point, for destination-to-source sampling in a renderer.\n *\n * The forward mapping must be monotonic, so the inverse is unique over the\n * declared operating radius. The exact centre, an identity profile and the\n * mapped boundary are solved directly. Other radii use a bounded,\n * deterministic bisection that stops when the floating-point bounds can no\n * longer narrow.\n */\nexport interface InverseRadialSourcePointValue {\n sourceImagePointMm: LensFieldPointMm;\n distortedNormalizedRadius: number;\n sourceNormalizedRadius: number;\n radialScaleAtSource: number;\n}\n\n/**\n * @internal Minimal inverse evaluation for composite batch models inside the\n * package that do not need the standalone radial delta or provenance wrapper.\n */\nexport function calculateInverseRadialSourcePointValue(\n distortedImagePointMm: LensFieldPointMm,\n profile: ValidatedRadialProfile,\n pointPath: string\n): InverseRadialSourcePointValue {\n requirePoint(pointPath, distortedImagePointMm);\n\n const distortedRadiusMm = Math.hypot(\n distortedImagePointMm.x,\n distortedImagePointMm.y\n );\n const distortedNormalizedRadius =\n distortedRadiusMm / profile.normalizationRadiusMm;\n requireFinite(\n `${pointPath} normalized radius`,\n distortedNormalizedRadius\n );\n\n const mappedRadiusTolerance = inverseMappedRadiusTolerance(\n distortedNormalizedRadius,\n profile.maximumMappedNormalizedRadius\n );\n if (\n distortedNormalizedRadius >\n profile.maximumMappedNormalizedRadius + mappedRadiusTolerance\n ) {\n throw new InvalidScientificInputError(\n `${pointPath} lies outside the mapped radial distortion profile operating radius.`\n );\n }\n const solverDistortedNormalizedRadius = Math.min(\n distortedNormalizedRadius,\n profile.maximumMappedNormalizedRadius\n );\n\n let sourceNormalizedRadius = 0;\n if (\n solverDistortedNormalizedRadius ===\n profile.maximumMappedNormalizedRadius\n ) {\n sourceNormalizedRadius = profile.maximumNormalizedRadius;\n } else if (solverDistortedNormalizedRadius > 0) {\n const { k1, k2, k3 } = profile.coefficients;\n if (k1 === 0 && k2 === 0 && k3 === 0) {\n sourceNormalizedRadius = solverDistortedNormalizedRadius;\n } else {\n let lower = 0;\n let upper = profile.maximumNormalizedRadius;\n\n for (\n let iteration = 0;\n iteration < INVERSE_BISECTION_ITERATIONS;\n iteration += 1\n ) {\n const midpoint = (lower + upper) / 2;\n if (midpoint === lower || midpoint === upper) {\n break;\n }\n\n const mapped = mappedNormalizedRadius(\n midpoint,\n profile.coefficients\n );\n if (mapped < solverDistortedNormalizedRadius) {\n lower = midpoint;\n } else {\n upper = midpoint;\n }\n }\n sourceNormalizedRadius = (lower + upper) / 2;\n }\n }\n\n const radialScaleAtSource = radialScale(\n sourceNormalizedRadius,\n profile.coefficients\n );\n const sourceImagePointMm =\n distortedNormalizedRadius === 0\n ? { x: 0, y: 0 }\n : solverDistortedNormalizedRadius === distortedNormalizedRadius\n ? {\n x: distortedImagePointMm.x / radialScaleAtSource,\n y: distortedImagePointMm.y / radialScaleAtSource\n }\n : {\n x:\n distortedImagePointMm.x *\n (sourceNormalizedRadius / distortedNormalizedRadius),\n y:\n distortedImagePointMm.y *\n (sourceNormalizedRadius / distortedNormalizedRadius)\n };\n\n return {\n sourceImagePointMm,\n distortedNormalizedRadius,\n sourceNormalizedRadius,\n radialScaleAtSource\n };\n}\n\nfunction calculateInverseRadialDistortionMappingValue(\n distortedImagePointMm: LensFieldPointMm,\n profile: ValidatedRadialProfile,\n pointPath: string\n): InverseRadialDistortionMapping {\n const source = calculateInverseRadialSourcePointValue(\n distortedImagePointMm,\n profile,\n pointPath\n );\n const deltaX =\n source.sourceImagePointMm.x - distortedImagePointMm.x;\n const deltaY =\n source.sourceImagePointMm.y - distortedImagePointMm.y;\n\n return {\n direction: \"distorted-to-undistorted\",\n distortedImagePointMm: { ...distortedImagePointMm },\n sourceImagePointMm: source.sourceImagePointMm,\n distortedNormalizedRadius: source.distortedNormalizedRadius,\n sourceNormalizedRadius: source.sourceNormalizedRadius,\n radialScaleAtSource: source.radialScaleAtSource,\n deltaMm: {\n x: deltaX,\n y: deltaY,\n distance: Math.hypot(deltaX, deltaY)\n },\n profileMinimumRadialDerivative: profile.minimumRadialDerivative\n };\n}\n\n/**\n * Inverts the declared monotonic radial field mapping inside its supported range, using the existing\n * bounded root solver. Optical coordinates are kept explicit.\n * @param input - CalculateInverseRadialDistortionMappingInput. See that type for coordinates, units and profile binding.\n * @returns CalculationResult<InverseRadialDistortionMapping>. The result states its shape and scientific status; a successful call does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the root export and exact type graph.\n */\nexport function calculateInverseRadialDistortionMapping(\n input: CalculateInverseRadialDistortionMappingInput\n): CalculationResult<InverseRadialDistortionMapping> {\n requireInputKeys(input, \"calculateInverseRadialDistortionMapping\", [\"distortedImagePointMm\", \"profile\"]);\n const profile = validateRadialDistortionProfile(input.profile);\n const mapping = calculateInverseRadialDistortionMappingValue(\n input.distortedImagePointMm,\n profile,\n \"distortedImagePointMm\"\n );\n\n return approximationResult(\n mapping,\n \"generic-inverse-radial-lens-distortion\",\n \"1.0.0\",\n [\n ...mappingAssumptions(),\n `Inverse radius uses exact center/identity/boundary solutions where applicable and otherwise up to ${INVERSE_BISECTION_ITERATIONS} deterministic bisection iterations, stopping when IEEE-754 bounds can no longer narrow`\n ]\n );\n}\n\n/**\n * Inverse-maps many distorted image-plane points at once. The radial profile\n * is validated and resolved only once for the whole batch.\n *\n * Each returned mapping is numerically identical to the single-point API.\n * Provenance and profile validation are reported once for the batch.\n */\nexport function calculateInverseRadialDistortionMappings(\n input: CalculateInverseRadialDistortionMappingsInput\n): CalculationResult<InverseRadialDistortionMappings> {\n requireInputKeys(input, \"calculateInverseRadialDistortionMappings\", [\"distortedImagePointsMm\", \"profile\"]);\n const profile = validateRadialDistortionProfile(input.profile);\n if (!Array.isArray(input.distortedImagePointsMm)) {\n throw new InvalidConfigurationError(\n \"distortedImagePointsMm must be an array of image-plane points.\"\n );\n }\n\n const mappings: InverseRadialDistortionMapping[] = [];\n for (\n let index = 0;\n index < input.distortedImagePointsMm.length;\n index += 1\n ) {\n const point = input.distortedImagePointsMm[index];\n if (point === undefined) {\n throw new InvalidScientificInputError(\n `distortedImagePointsMm[${index}] must be an image-plane point.`\n );\n }\n mappings.push(\n calculateInverseRadialDistortionMappingValue(\n point,\n profile,\n `distortedImagePointsMm[${index}]`\n )\n );\n }\n\n return approximationResult(\n {\n direction: \"distorted-to-undistorted-batch\",\n mappings,\n pointCount: mappings.length,\n profileMinimumRadialDerivative: profile.minimumRadialDerivative\n },\n \"generic-inverse-radial-lens-distortion-batch\",\n \"1.0.0\",\n [\n ...mappingAssumptions(),\n `Inverse radius uses exact center/identity/boundary solutions where applicable and otherwise up to ${INVERSE_BISECTION_ITERATIONS} deterministic bisection iterations per point, stopping when IEEE-754 bounds can no longer narrow`,\n \"The radial profile is validated and its invariant extrema are resolved once for the batch\"\n ]\n );\n}\n"]}
|
|
1
|
+
{"version":3,"file":"radial-distortion.js","sourceRoot":"","sources":["../../src/optics/radial-distortion.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Generic radial lens distortion (barrel and pincushion).\n *\n * The mapping is rotationally symmetric about the optical axis:\n * p_distorted = p_ideal * (1 + k1 r² + k2 r⁴ + k3 r⁶)\n * where r is normalized by the physical reference radius the caller declares.\n *\n * A profile is accepted only if the radial mapping is strictly monotonic over its declared operating\n * radius. That makes the inverse mapping (destination-to-source sampling) well defined.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport {\n approximationResult,\n type CalculationResult\n} from \"../core/calculation-result.js\";\nimport {\n InvalidScientificInputError,\n requirePositiveFinite\n} from \"../core/validation.js\";\nimport { requireInputObject, requireInputKeys, assertNamedFinite as requireFinite } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\nexport interface RadialDistortionCoefficients {\n /** Dimensionless r² coefficient. */\n k1: number;\n /** Dimensionless r⁴ coefficient. */\n k2: number;\n /** Dimensionless r⁶ coefficient. */\n k3: number;\n}\n\nexport interface RadialDistortionProfile {\n /**\n * Physical image-plane radius used to normalize field position, in mm.\n * Coefficients are meaningful only with this declared normalization.\n */\n normalizationRadiusMm: number;\n /**\n * Largest undistorted normalized radius for which this profile is declared\n * valid. Within it the mapping must stay one-to-one and invertible.\n */\n maximumNormalizedRadius: number;\n coefficients: RadialDistortionCoefficients;\n}\n\nexport interface LensFieldPointMm {\n /** Image-plane X coordinate relative to the optical axis, in mm. */\n x: number;\n /** Image-plane Y coordinate relative to the optical axis, in mm. */\n y: number;\n}\n\nexport interface CalculateRadialDistortionMappingInput {\n /** Ideal/undistorted image-plane point. */\n imagePointMm: LensFieldPointMm;\n profile: RadialDistortionProfile;\n}\n\nexport interface CalculateInverseRadialDistortionMappingInput {\n /** Distorted image-plane point to inverse-map back to ideal coordinates. */\n distortedImagePointMm: LensFieldPointMm;\n profile: RadialDistortionProfile;\n}\n\nexport interface CalculateInverseRadialDistortionMappingsInput {\n /** Distorted image-plane destinations to inverse-map in one validated batch. */\n distortedImagePointsMm: readonly LensFieldPointMm[];\n profile: RadialDistortionProfile;\n}\n\nexport interface RadialDistortionMapping {\n direction: \"undistorted-to-distorted\";\n sourceImagePointMm: LensFieldPointMm;\n mappedImagePointMm: LensFieldPointMm;\n sourceNormalizedRadius: number;\n mappedNormalizedRadius: number;\n radialScale: number;\n deltaMm: {\n x: number;\n y: number;\n distance: number;\n };\n profileMinimumRadialDerivative: number;\n}\n\nexport interface InverseRadialDistortionMapping {\n direction: \"distorted-to-undistorted\";\n distortedImagePointMm: LensFieldPointMm;\n sourceImagePointMm: LensFieldPointMm;\n distortedNormalizedRadius: number;\n sourceNormalizedRadius: number;\n radialScaleAtSource: number;\n deltaMm: {\n x: number;\n y: number;\n distance: number;\n };\n profileMinimumRadialDerivative: number;\n}\n\nexport interface InverseRadialDistortionMappings {\n direction: \"distorted-to-undistorted-batch\";\n mappings: readonly InverseRadialDistortionMapping[];\n pointCount: number;\n profileMinimumRadialDerivative: number;\n}\n\n/** @internal Shared by the composite lens-field batch evaluators. Not exported from the package root. */\nexport interface ValidatedRadialProfile {\n normalizationRadiusMm: number;\n maximumNormalizedRadius: number;\n coefficients: RadialDistortionCoefficients;\n minimumRadialDerivative: number;\n maximumMappedNormalizedRadius: number;\n}\n\nconst INVERSE_BISECTION_ITERATIONS = 80;\nconst INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS = 16;\n\nfunction inverseMappedRadiusTolerance(\n distortedNormalizedRadius: number,\n maximumMappedNormalizedRadius: number\n): number {\n return (\n Number.EPSILON *\n INVERSE_MAPPED_RADIUS_FLOATING_TOLERANCE_ULPS *\n Math.max(\n 1,\n Math.abs(distortedNormalizedRadius),\n Math.abs(maximumMappedNormalizedRadius)\n )\n );\n}\n\nfunction radialScale(\n normalizedRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n const r2 = normalizedRadius * normalizedRadius;\n const r4 = r2 * r2;\n const r6 = r4 * r2;\n return (\n 1 +\n coefficients.k1 * r2 +\n coefficients.k2 * r4 +\n coefficients.k3 * r6\n );\n}\n\nfunction mappedNormalizedRadius(\n normalizedRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n return normalizedRadius * radialScale(normalizedRadius, coefficients);\n}\n\nfunction radialDerivativeFromSquaredRadius(\n squaredRadius: number,\n coefficients: RadialDistortionCoefficients\n): number {\n return (\n 1 +\n 3 * coefficients.k1 * squaredRadius +\n 5 * coefficients.k2 * squaredRadius * squaredRadius +\n 7 *\n coefficients.k3 *\n squaredRadius *\n squaredRadius *\n squaredRadius\n );\n}\n\nfunction derivativeCriticalSquaredRadii(\n coefficients: RadialDistortionCoefficients\n): number[] {\n // d/dr² of the radial derivative:\n // 3 k1 + 10 k2 s + 21 k3 s² = 0, where s = r².\n //\n // Scale the quadratic before solving, so finite caller coefficients cannot\n // overflow the discriminant. Use the numerically stable q-form roots. Then a\n // tiny quadratic term cannot erase the smaller physical root through\n // cancellation and let a non-monotonic profile pass by accident.\n const a = 21 * coefficients.k3;\n const b = 10 * coefficients.k2;\n const c = 3 * coefficients.k1;\n const coefficientScale = Math.max(\n Math.abs(a),\n Math.abs(b),\n Math.abs(c)\n );\n\n if (coefficientScale === 0) {\n return [];\n }\n\n const scaledA = a / coefficientScale;\n const scaledB = b / coefficientScale;\n const scaledC = c / coefficientScale;\n\n if (scaledA === 0) {\n return scaledB === 0 ? [] : [-scaledC / scaledB];\n }\n\n const squaredB = scaledB * scaledB;\n const fourAC = 4 * scaledA * scaledC;\n const discriminant = squaredB - fourAC;\n const discriminantTolerance =\n Number.EPSILON *\n 16 *\n Math.max(1, Math.abs(squaredB), Math.abs(fourAC));\n\n if (discriminant < -discriminantTolerance) {\n return [];\n }\n\n const root = Math.sqrt(Math.max(0, discriminant));\n if (root === 0) {\n return [-scaledB / (2 * scaledA)];\n }\n\n // root > 0 here, so q is never zero: for scaledB >= 0 the sum scaledB + root is positive and for\n // scaledB < 0 the difference scaledB - root is negative. (A former q === 0 branch was unreachable.)\n const q =\n -0.5 *\n (scaledB + (scaledB >= 0 ? root : -root));\n\n return [q / scaledA, scaledC / q];\n}\n\n/** @internal Validates and copies one profile for repeated use inside the package. */\nexport function validateRadialDistortionProfile(\n profile: RadialDistortionProfile\n): ValidatedRadialProfile {\n requireInputObject(profile, \"profile input\");\n requirePositiveFinite(\n \"profile.normalizationRadiusMm\",\n profile.normalizationRadiusMm\n );\n requirePositiveFinite(\n \"profile.maximumNormalizedRadius\",\n profile.maximumNormalizedRadius\n );\n requireFinite(\"profile.coefficients.k1\", profile.coefficients.k1);\n requireFinite(\"profile.coefficients.k2\", profile.coefficients.k2);\n requireFinite(\"profile.coefficients.k3\", profile.coefficients.k3);\n\n const maximumSquaredRadius =\n profile.maximumNormalizedRadius * profile.maximumNormalizedRadius;\n if (!Number.isFinite(maximumSquaredRadius)) {\n throw new InvalidScientificInputError(\n \"profile.maximumNormalizedRadius is too large to evaluate safely.\"\n );\n }\n\n const candidates = [\n 0,\n maximumSquaredRadius,\n ...derivativeCriticalSquaredRadii(profile.coefficients).filter(\n (value) => value > 0 && value < maximumSquaredRadius\n )\n ];\n const minimumRadialDerivative = Math.min(\n ...candidates.map((squaredRadius) =>\n radialDerivativeFromSquaredRadius(\n squaredRadius,\n profile.coefficients\n )\n )\n );\n\n if (\n !Number.isFinite(minimumRadialDerivative) ||\n minimumRadialDerivative <= 0\n ) {\n throw new InvalidScientificInputError(\n \"Radial distortion profile must remain strictly monotonic over maximumNormalizedRadius so inverse mapping is single-valued.\"\n );\n }\n\n const maximumMappedRadius = mappedNormalizedRadius(\n profile.maximumNormalizedRadius,\n profile.coefficients\n );\n if (!Number.isFinite(maximumMappedRadius) || maximumMappedRadius <= 0) {\n throw new InvalidScientificInputError(\n \"Radial distortion profile produces an invalid mapped operating radius.\"\n );\n }\n\n return {\n normalizationRadiusMm: profile.normalizationRadiusMm,\n maximumNormalizedRadius: profile.maximumNormalizedRadius,\n coefficients: { ...profile.coefficients },\n minimumRadialDerivative,\n maximumMappedNormalizedRadius: maximumMappedRadius\n };\n}\n\nfunction requirePoint(name: string, point: LensFieldPointMm): void {\n if (typeof point !== \"object\" || point === null) {\n throw new InvalidConfigurationError(\n `${name} must be an object with finite numeric x and y coordinates.`\n );\n }\n if (\n typeof point.x !== \"number\" ||\n typeof point.y !== \"number\" ||\n !Number.isFinite(point.x) ||\n !Number.isFinite(point.y)\n ) {\n throw new InvalidScientificInputError(\n `${name} must contain finite numeric x and y coordinates.`\n );\n }\n}\n\nfunction mappingAssumptions(): readonly string[] {\n return [\n \"Generic rotationally symmetric radial polynomial field mapping\",\n \"Distortion is centered on the optical axis; decentering and tangential distortion are not modeled\",\n \"Normalized radius is image-plane radius divided by the caller-declared normalizationRadiusMm\",\n \"Radial scale is 1 + k1 r^2 + k2 r^4 + k3 r^6\",\n \"The declared operating radius is required to remain strictly monotonic so inverse mapping is single-valued\",\n \"Coefficients are generic caller inputs and do not represent a named lens unless separately calibrated with defensible provenance\"\n ];\n}\n\n/**\n * Applies a generic radial lens-distortion field mapping to one ideal\n * image-plane point.\n *\n * The mapping is rotationally symmetric about the optical axis:\n *\n * p_distorted = p_ideal * (1 + k1 r² + k2 r⁴ + k3 r⁶)\n *\n * where r is normalized by the caller-declared physical reference radius.\n *\n * A profile is accepted only if the radial mapping is strictly monotonic over\n * its declared operating radius. That makes the inverse mapping\n * (destination-to-source sampling) well defined.\n */\nexport function calculateRadialDistortionMapping(\n input: CalculateRadialDistortionMappingInput\n): CalculationResult<RadialDistortionMapping> {\n requireInputKeys(input, \"calculateRadialDistortionMapping\", [\"imagePointMm\", \"profile\"]);\n requirePoint(\"imagePointMm\", input.imagePointMm);\n const profile = validateRadialDistortionProfile(input.profile);\n\n const sourceRadiusMm = Math.hypot(\n input.imagePointMm.x,\n input.imagePointMm.y\n );\n const sourceNormalizedRadius =\n sourceRadiusMm / profile.normalizationRadiusMm;\n requireFinite(\"imagePointMm normalized radius\", sourceNormalizedRadius);\n\n if (sourceNormalizedRadius > profile.maximumNormalizedRadius) {\n throw new InvalidScientificInputError(\n \"imagePointMm lies outside the radial distortion profile maximumNormalizedRadius.\"\n );\n }\n\n const scale = radialScale(\n sourceNormalizedRadius,\n profile.coefficients\n );\n const mappedImagePointMm = {\n x: input.imagePointMm.x * scale,\n y: input.imagePointMm.y * scale\n };\n const mappedRadius = sourceNormalizedRadius * scale;\n const deltaX = mappedImagePointMm.x - input.imagePointMm.x;\n const deltaY = mappedImagePointMm.y - input.imagePointMm.y;\n\n return approximationResult(\n {\n direction: \"undistorted-to-distorted\",\n sourceImagePointMm: { ...input.imagePointMm },\n mappedImagePointMm,\n sourceNormalizedRadius,\n mappedNormalizedRadius: mappedRadius,\n radialScale: scale,\n deltaMm: {\n x: deltaX,\n y: deltaY,\n distance: Math.hypot(deltaX, deltaY)\n },\n profileMinimumRadialDerivative:\n profile.minimumRadialDerivative\n },\n \"generic-radial-lens-distortion\",\n \"1.0.0\",\n mappingAssumptions()\n );\n}\n\n/**\n * Inverse-maps one distorted image-plane point to its ideal (undistorted)\n * source point, for destination-to-source sampling in a renderer.\n *\n * The forward mapping must be monotonic, so the inverse is unique over the\n * declared operating radius. The exact centre, an identity profile and the\n * mapped boundary are solved directly. Other radii use a bounded,\n * deterministic bisection that stops when the floating-point bounds can no\n * longer narrow.\n */\nexport interface InverseRadialSourcePointValue {\n sourceImagePointMm: LensFieldPointMm;\n distortedNormalizedRadius: number;\n sourceNormalizedRadius: number;\n radialScaleAtSource: number;\n}\n\n/**\n * @internal Minimal inverse evaluation for composite batch models inside the\n * package that do not need the standalone radial delta or provenance wrapper.\n */\nexport function calculateInverseRadialSourcePointValue(\n distortedImagePointMm: LensFieldPointMm,\n profile: ValidatedRadialProfile,\n pointPath: string\n): InverseRadialSourcePointValue {\n requirePoint(pointPath, distortedImagePointMm);\n\n const distortedRadiusMm = Math.hypot(\n distortedImagePointMm.x,\n distortedImagePointMm.y\n );\n const distortedNormalizedRadius =\n distortedRadiusMm / profile.normalizationRadiusMm;\n requireFinite(\n `${pointPath} normalized radius`,\n distortedNormalizedRadius\n );\n\n const mappedRadiusTolerance = inverseMappedRadiusTolerance(\n distortedNormalizedRadius,\n profile.maximumMappedNormalizedRadius\n );\n if (\n distortedNormalizedRadius >\n profile.maximumMappedNormalizedRadius + mappedRadiusTolerance\n ) {\n throw new InvalidScientificInputError(\n `${pointPath} lies outside the mapped radial distortion profile operating radius.`\n );\n }\n const solverDistortedNormalizedRadius = Math.min(\n distortedNormalizedRadius,\n profile.maximumMappedNormalizedRadius\n );\n\n let sourceNormalizedRadius = 0;\n if (\n solverDistortedNormalizedRadius ===\n profile.maximumMappedNormalizedRadius\n ) {\n sourceNormalizedRadius = profile.maximumNormalizedRadius;\n } else if (solverDistortedNormalizedRadius > 0) {\n const { k1, k2, k3 } = profile.coefficients;\n if (k1 === 0 && k2 === 0 && k3 === 0) {\n sourceNormalizedRadius = solverDistortedNormalizedRadius;\n } else {\n let lower = 0;\n let upper = profile.maximumNormalizedRadius;\n\n for (\n let iteration = 0;\n iteration < INVERSE_BISECTION_ITERATIONS;\n iteration += 1\n ) {\n const midpoint = (lower + upper) / 2;\n if (midpoint === lower || midpoint === upper) {\n break;\n }\n\n const mapped = mappedNormalizedRadius(\n midpoint,\n profile.coefficients\n );\n if (mapped < solverDistortedNormalizedRadius) {\n lower = midpoint;\n } else {\n upper = midpoint;\n }\n }\n sourceNormalizedRadius = (lower + upper) / 2;\n }\n }\n\n const radialScaleAtSource = radialScale(\n sourceNormalizedRadius,\n profile.coefficients\n );\n const sourceImagePointMm =\n distortedNormalizedRadius === 0\n ? { x: 0, y: 0 }\n : solverDistortedNormalizedRadius === distortedNormalizedRadius\n ? {\n x: distortedImagePointMm.x / radialScaleAtSource,\n y: distortedImagePointMm.y / radialScaleAtSource\n }\n : {\n x:\n distortedImagePointMm.x *\n (sourceNormalizedRadius / distortedNormalizedRadius),\n y:\n distortedImagePointMm.y *\n (sourceNormalizedRadius / distortedNormalizedRadius)\n };\n\n return {\n sourceImagePointMm,\n distortedNormalizedRadius,\n sourceNormalizedRadius,\n radialScaleAtSource\n };\n}\n\nfunction calculateInverseRadialDistortionMappingValue(\n distortedImagePointMm: LensFieldPointMm,\n profile: ValidatedRadialProfile,\n pointPath: string\n): InverseRadialDistortionMapping {\n const source = calculateInverseRadialSourcePointValue(\n distortedImagePointMm,\n profile,\n pointPath\n );\n const deltaX =\n source.sourceImagePointMm.x - distortedImagePointMm.x;\n const deltaY =\n source.sourceImagePointMm.y - distortedImagePointMm.y;\n\n return {\n direction: \"distorted-to-undistorted\",\n distortedImagePointMm: { ...distortedImagePointMm },\n sourceImagePointMm: source.sourceImagePointMm,\n distortedNormalizedRadius: source.distortedNormalizedRadius,\n sourceNormalizedRadius: source.sourceNormalizedRadius,\n radialScaleAtSource: source.radialScaleAtSource,\n deltaMm: {\n x: deltaX,\n y: deltaY,\n distance: Math.hypot(deltaX, deltaY)\n },\n profileMinimumRadialDerivative: profile.minimumRadialDerivative\n };\n}\n\n/**\n * Inverts the declared monotonic radial field mapping inside its supported range, using the existing\n * bounded root solver. Optical coordinates are kept explicit.\n * @param input - CalculateInverseRadialDistortionMappingInput. See that type for coordinates, units and profile binding.\n * @returns CalculationResult<InverseRadialDistortionMapping>. The result states its shape and scientific status; a successful call does not imply calibration.\n *\n * @see docs/API_REFERENCE.md for the root export and exact type graph.\n */\nexport function calculateInverseRadialDistortionMapping(\n input: CalculateInverseRadialDistortionMappingInput\n): CalculationResult<InverseRadialDistortionMapping> {\n requireInputKeys(input, \"calculateInverseRadialDistortionMapping\", [\"distortedImagePointMm\", \"profile\"]);\n const profile = validateRadialDistortionProfile(input.profile);\n const mapping = calculateInverseRadialDistortionMappingValue(\n input.distortedImagePointMm,\n profile,\n \"distortedImagePointMm\"\n );\n\n return approximationResult(\n mapping,\n \"generic-inverse-radial-lens-distortion\",\n \"1.0.0\",\n [\n ...mappingAssumptions(),\n `Inverse radius uses exact center/identity/boundary solutions where applicable and otherwise up to ${INVERSE_BISECTION_ITERATIONS} deterministic bisection iterations, stopping when IEEE-754 bounds can no longer narrow`\n ]\n );\n}\n\n/**\n * Inverse-maps many distorted image-plane points at once. The radial profile\n * is validated and resolved only once for the whole batch.\n *\n * Each returned mapping is numerically identical to the single-point API.\n * Provenance and profile validation are reported once for the batch.\n */\nexport function calculateInverseRadialDistortionMappings(\n input: CalculateInverseRadialDistortionMappingsInput\n): CalculationResult<InverseRadialDistortionMappings> {\n requireInputKeys(input, \"calculateInverseRadialDistortionMappings\", [\"distortedImagePointsMm\", \"profile\"]);\n const profile = validateRadialDistortionProfile(input.profile);\n if (!Array.isArray(input.distortedImagePointsMm)) {\n throw new InvalidConfigurationError(\n \"distortedImagePointsMm must be an array of image-plane points.\"\n );\n }\n\n const mappings: InverseRadialDistortionMapping[] = [];\n for (\n let index = 0;\n index < input.distortedImagePointsMm.length;\n index += 1\n ) {\n const point = input.distortedImagePointsMm[index];\n if (point === undefined) {\n throw new InvalidScientificInputError(\n `distortedImagePointsMm[${index}] must be an image-plane point.`\n );\n }\n mappings.push(\n calculateInverseRadialDistortionMappingValue(\n point,\n profile,\n `distortedImagePointsMm[${index}]`\n )\n );\n }\n\n return approximationResult(\n {\n direction: \"distorted-to-undistorted-batch\",\n mappings,\n pointCount: mappings.length,\n profileMinimumRadialDerivative: profile.minimumRadialDerivative\n },\n \"generic-inverse-radial-lens-distortion-batch\",\n \"1.0.0\",\n [\n ...mappingAssumptions(),\n `Inverse radius uses exact center/identity/boundary solutions where applicable and otherwise up to ${INVERSE_BISECTION_ITERATIONS} deterministic bisection iterations per point, stopping when IEEE-754 bounds can no longer narrow`,\n \"The radial profile is validated and its invariant extrema are resolved once for the batch\"\n ]\n );\n}\n"]}
|
|
@@ -23,74 +23,14 @@
|
|
|
23
23
|
import { approximationResult } from "../core/calculation-result.js";
|
|
24
24
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
25
25
|
import { parseEvidenceList } from "../core/evidence-provenance.js";
|
|
26
|
-
import {
|
|
26
|
+
import {} from "../core/spectral.js";
|
|
27
27
|
import { InvalidScientificInputError } from "../core/validation.js";
|
|
28
|
-
import { composeFrontOfLensFilterTransmission } from "./front-of-lens-filter.js";
|
|
28
|
+
import { composeFrontOfLensFilterTransmission, parseRange, parseResolvedBasis } from "./front-of-lens-filter.js";
|
|
29
29
|
import { calculateThinLensImageDistance } from "./thin-lens.js";
|
|
30
30
|
import { parseSceneRadianceEvaluationRequest, parseSceneRadianceEvaluationResult } from "../schema/scene-radiance.js";
|
|
31
|
-
import { requireInputKeys,
|
|
31
|
+
import { requireInputKeys, rejectUnknownVariantFields, UNCERTAINTY_VARIANT_FIELDS, requireConfigRecord as requireRecord, requireTrimmedConfigString as requireNonEmptyString, parseConfigScientificStatus as parseScientificStatus, requireInputPositiveFinite as requirePositiveFiniteScientific, parseConfigLimitations as parseLimitations, requireConfigFiniteAboveZero as requirePositiveFinite } from "../core/record-validation.js";
|
|
32
|
+
import { requireFraction } from "../color/white-balance.js";
|
|
32
33
|
export const SCENE_TO_SENSOR_IRRADIANCE_PROFILE_SCHEMA_VERSION = "0.1.0";
|
|
33
|
-
function requireRecord(value, path, allowedKeys) {
|
|
34
|
-
if (typeof value !== "object" ||
|
|
35
|
-
value === null ||
|
|
36
|
-
Array.isArray(value)) {
|
|
37
|
-
throw new InvalidConfigurationError(path + " must be an object.");
|
|
38
|
-
}
|
|
39
|
-
if (allowedKeys !== null)
|
|
40
|
-
rejectUnknownFields(value, allowedKeys, path);
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
function requireNonEmptyString(value, path) {
|
|
44
|
-
if (typeof value !== "string" ||
|
|
45
|
-
value.trim().length === 0) {
|
|
46
|
-
throw new InvalidConfigurationError(path + " must be a non-empty string.");
|
|
47
|
-
}
|
|
48
|
-
return value.trim();
|
|
49
|
-
}
|
|
50
|
-
function requireFinite(value, path) {
|
|
51
|
-
if (typeof value !== "number" ||
|
|
52
|
-
!Number.isFinite(value)) {
|
|
53
|
-
throw new InvalidConfigurationError(path + " must be finite.");
|
|
54
|
-
}
|
|
55
|
-
return value;
|
|
56
|
-
}
|
|
57
|
-
function requirePositiveFinite(value, path) {
|
|
58
|
-
const parsed = requireFinite(value, path);
|
|
59
|
-
if (parsed <= 0) {
|
|
60
|
-
throw new InvalidConfigurationError(path +
|
|
61
|
-
" must be greater than zero.");
|
|
62
|
-
}
|
|
63
|
-
return parsed;
|
|
64
|
-
}
|
|
65
|
-
function requireFraction(value, path) {
|
|
66
|
-
const parsed = requireFinite(value, path);
|
|
67
|
-
if (parsed < 0 ||
|
|
68
|
-
parsed > 1) {
|
|
69
|
-
throw new InvalidConfigurationError(path +
|
|
70
|
-
" must be from zero through one.");
|
|
71
|
-
}
|
|
72
|
-
return parsed;
|
|
73
|
-
}
|
|
74
|
-
function parseRange(value, path) {
|
|
75
|
-
const record = requireRecord(value, path, ["minimum", "maximum"]);
|
|
76
|
-
const minimum = requirePositiveFinite(record.minimum, path + ".minimum");
|
|
77
|
-
const maximum = requirePositiveFinite(record.maximum, path + ".maximum");
|
|
78
|
-
if (minimum > maximum) {
|
|
79
|
-
throw new InvalidConfigurationError(path +
|
|
80
|
-
".minimum must be less than or equal to maximum.");
|
|
81
|
-
}
|
|
82
|
-
return {
|
|
83
|
-
minimum,
|
|
84
|
-
maximum
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
function parseScientificStatus(value, path) {
|
|
88
|
-
if (value !== "calibrated" &&
|
|
89
|
-
value !== "approximation") {
|
|
90
|
-
throw new InvalidConfigurationError(path + " is invalid.");
|
|
91
|
-
}
|
|
92
|
-
return value;
|
|
93
|
-
}
|
|
94
34
|
function parseUncertainty(value, path) {
|
|
95
35
|
const record = requireRecord(value, path, null);
|
|
96
36
|
rejectUnknownVariantFields(record, "kind", UNCERTAINTY_VARIANT_FIELDS, path);
|
|
@@ -110,21 +50,6 @@ function parseUncertainty(value, path) {
|
|
|
110
50
|
}
|
|
111
51
|
throw new InvalidConfigurationError(path + ".kind is invalid.");
|
|
112
52
|
}
|
|
113
|
-
function parseLimitations(value, path) {
|
|
114
|
-
if (!Array.isArray(value)) {
|
|
115
|
-
throw new InvalidConfigurationError(path + " must be an array.");
|
|
116
|
-
}
|
|
117
|
-
const parsed = value.map((entry, index) => requireNonEmptyString(entry, path +
|
|
118
|
-
"[" +
|
|
119
|
-
index +
|
|
120
|
-
"]"));
|
|
121
|
-
if (new Set(parsed).size !==
|
|
122
|
-
parsed.length) {
|
|
123
|
-
throw new InvalidConfigurationError(path +
|
|
124
|
-
" must not contain duplicates.");
|
|
125
|
-
}
|
|
126
|
-
return parsed;
|
|
127
|
-
}
|
|
128
53
|
function parseReusableNumericEvidence(value, path) {
|
|
129
54
|
const evidence = parseEvidenceList(value, path);
|
|
130
55
|
if (evidence.some((entry) => entry.reuseStatus ===
|
|
@@ -238,14 +163,6 @@ function parseTransmission(value, path) {
|
|
|
238
163
|
}
|
|
239
164
|
throw new InvalidConfigurationError(path + ".kind is invalid.");
|
|
240
165
|
}
|
|
241
|
-
function parseResolvedBasis(value, path) {
|
|
242
|
-
const parsed = parseSpectralWavelengthBasis(value, path);
|
|
243
|
-
if (parsed === "unspecified") {
|
|
244
|
-
throw new InvalidConfigurationError(path +
|
|
245
|
-
' must resolve to "air" or "vacuum".');
|
|
246
|
-
}
|
|
247
|
-
return parsed;
|
|
248
|
-
}
|
|
249
166
|
function parseFocusApplicability(value, path) {
|
|
250
167
|
const record = requireRecord(value, path, null);
|
|
251
168
|
rejectUnknownVariantFields(record, "kind", { any: ["kind"], "finite-distance-range": ["kind", "objectDistanceM"] }, path);
|
|
@@ -381,15 +298,6 @@ function workingFNumber(focus, focalLengthMm, nominalFNumber) {
|
|
|
381
298
|
}
|
|
382
299
|
throw new InvalidConfigurationError("focus.kind is invalid.");
|
|
383
300
|
}
|
|
384
|
-
function requirePositiveFiniteScientific(value, path) {
|
|
385
|
-
if (typeof value !== "number" ||
|
|
386
|
-
!Number.isFinite(value) ||
|
|
387
|
-
value <= 0) {
|
|
388
|
-
throw new InvalidScientificInputError(path +
|
|
389
|
-
" must be finite and greater than zero.");
|
|
390
|
-
}
|
|
391
|
-
return value;
|
|
392
|
-
}
|
|
393
301
|
function finiteFocusDistance(focus) {
|
|
394
302
|
if (focus.kind ===
|
|
395
303
|
"ideal-symmetric-thin-lens") {
|