@photivra/engine 0.2.0 → 0.5.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.
Files changed (89) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/CITATION.cff +1 -1
  3. package/README.md +59 -10
  4. package/dist/camera/equivalent-focal-length.d.ts +34 -0
  5. package/dist/camera/equivalent-focal-length.d.ts.map +1 -0
  6. package/dist/camera/equivalent-focal-length.js +33 -0
  7. package/dist/camera/equivalent-focal-length.js.map +1 -0
  8. package/dist/camera/field-of-view.d.ts +40 -3
  9. package/dist/camera/field-of-view.d.ts.map +1 -1
  10. package/dist/camera/field-of-view.js +66 -24
  11. package/dist/camera/field-of-view.js.map +1 -1
  12. package/dist/core/calculation-result.d.ts.map +1 -1
  13. package/dist/core/calculation-result.js +14 -2
  14. package/dist/core/calculation-result.js.map +1 -1
  15. package/dist/core/configuration-error.d.ts +9 -0
  16. package/dist/core/configuration-error.d.ts.map +1 -0
  17. package/dist/core/configuration-error.js +13 -0
  18. package/dist/core/configuration-error.js.map +1 -0
  19. package/dist/core/evidence-provenance.d.ts +40 -0
  20. package/dist/core/evidence-provenance.d.ts.map +1 -0
  21. package/dist/core/evidence-provenance.js +79 -0
  22. package/dist/core/evidence-provenance.js.map +1 -0
  23. package/dist/core/image-formation.d.ts +70 -0
  24. package/dist/core/image-formation.d.ts.map +1 -0
  25. package/dist/core/image-formation.js +335 -0
  26. package/dist/core/image-formation.js.map +1 -0
  27. package/dist/index.d.ts +15 -3
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +15 -3
  30. package/dist/index.js.map +1 -1
  31. package/dist/motion/camera-rotation.d.ts +91 -0
  32. package/dist/motion/camera-rotation.d.ts.map +1 -0
  33. package/dist/motion/camera-rotation.js +157 -0
  34. package/dist/motion/camera-rotation.js.map +1 -0
  35. package/dist/optics/focus-breathing.d.ts +73 -0
  36. package/dist/optics/focus-breathing.d.ts.map +1 -0
  37. package/dist/optics/focus-breathing.js +77 -0
  38. package/dist/optics/focus-breathing.js.map +1 -0
  39. package/dist/optics/illumination-vignetting.d.ts +59 -0
  40. package/dist/optics/illumination-vignetting.d.ts.map +1 -0
  41. package/dist/optics/illumination-vignetting.js +107 -0
  42. package/dist/optics/illumination-vignetting.js.map +1 -0
  43. package/dist/optics/lateral-chromatic-aberration.d.ts +117 -0
  44. package/dist/optics/lateral-chromatic-aberration.d.ts.map +1 -0
  45. package/dist/optics/lateral-chromatic-aberration.js +228 -0
  46. package/dist/optics/lateral-chromatic-aberration.js.map +1 -0
  47. package/dist/optics/psf-foundation.d.ts +102 -0
  48. package/dist/optics/psf-foundation.d.ts.map +1 -0
  49. package/dist/optics/psf-foundation.js +164 -0
  50. package/dist/optics/psf-foundation.js.map +1 -0
  51. package/dist/optics/radial-distortion.d.ts +131 -0
  52. package/dist/optics/radial-distortion.d.ts.map +1 -0
  53. package/dist/optics/radial-distortion.js +227 -0
  54. package/dist/optics/radial-distortion.js.map +1 -0
  55. package/dist/output/capture-geometry.d.ts +278 -0
  56. package/dist/output/capture-geometry.d.ts.map +1 -0
  57. package/dist/output/capture-geometry.js +562 -0
  58. package/dist/output/capture-geometry.js.map +1 -0
  59. package/dist/schema/validation.d.ts +1 -8
  60. package/dist/schema/validation.d.ts.map +1 -1
  61. package/dist/schema/validation.js +2 -11
  62. package/dist/schema/validation.js.map +1 -1
  63. package/dist/sensor/architecture.d.ts +40 -0
  64. package/dist/sensor/architecture.d.ts.map +1 -0
  65. package/dist/sensor/architecture.js +98 -0
  66. package/dist/sensor/architecture.js.map +1 -0
  67. package/dist/sensor/radiometry-readiness.d.ts +111 -0
  68. package/dist/sensor/radiometry-readiness.d.ts.map +1 -0
  69. package/dist/sensor/radiometry-readiness.js +294 -0
  70. package/dist/sensor/radiometry-readiness.js.map +1 -0
  71. package/dist/sensor/sensor-geometry.d.ts +99 -0
  72. package/dist/sensor/sensor-geometry.d.ts.map +1 -0
  73. package/dist/sensor/sensor-geometry.js +71 -0
  74. package/dist/sensor/sensor-geometry.js.map +1 -0
  75. package/dist/simulation/poc-simulation.d.ts +93 -1
  76. package/dist/simulation/poc-simulation.d.ts.map +1 -1
  77. package/dist/simulation/poc-simulation.js +308 -24
  78. package/dist/simulation/poc-simulation.js.map +1 -1
  79. package/docs/API_STYLE.md +36 -2
  80. package/docs/ARCHITECTURE.md +179 -1
  81. package/docs/IMAGE_FORMATION.md +151 -0
  82. package/docs/MOTION_AND_SIGNAL.md +52 -1
  83. package/docs/PHYSICS_FOUNDATION.md +190 -3
  84. package/docs/POC_API.md +73 -7
  85. package/docs/PROVENANCE.md +24 -0
  86. package/docs/PSF_FOUNDATION.md +133 -0
  87. package/docs/STABILIZATION.md +17 -4
  88. package/docs/USAGE.md +714 -3
  89. package/package.json +3 -2
package/dist/index.js CHANGED
@@ -5,23 +5,35 @@
5
5
  * This constant is intentionally independent of the package version so
6
6
  * schema/API compatibility can be discussed explicitly.
7
7
  */
8
- export const ENGINE_API_VERSION = "0.17.0";
8
+ export const ENGINE_API_VERSION = "0.34.0";
9
9
  export { approximationResult, calibratedResult, calculatedResult, estimatedResult, validateCalculationQuality } from "./core/calculation-result.js";
10
10
  export { InvalidScientificInputError, InvalidScientificResultError } from "./core/validation.js";
11
- export { calculateFieldOfView } from "./camera/field-of-view.js";
11
+ export { IMAGE_FORMATION_CONTRACT_VERSION, getImageFormationContract } from "./core/image-formation.js";
12
+ export { calculateFieldOfView, calculateFieldOfViewBounds } from "./camera/field-of-view.js";
12
13
  export { calculateProjectedObjectSize } from "./camera/projected-object-size.js";
14
+ export { calculateEquivalentFocalLength35Mm } from "./camera/equivalent-focal-length.js";
13
15
  export { calculatePixelPitch } from "./sensor/pixel-pitch.js";
16
+ export { calculateImagingAreaMetrics, calculateSensorGeometryMetrics } from "./sensor/sensor-geometry.js";
17
+ export { parseSensorArchitectureProfile } from "./sensor/architecture.js";
14
18
  export { calculateCenteredCrop } from "./output/crop.js";
15
19
  export { calculateSubjectFramingCrop } from "./output/subject-framing-crop.js";
20
+ export { calculateActiveCaptureFieldOfView, calculateOutputFieldOfView, mapImagePlanePointToOrientedPhysicalUv, mapOrientedPhysicalUvToImagePlanePoint, resolveCaptureGeometry, transformNativeRasterPointToOriented, transformNativeRasterRectToOriented, transformNativeRasterVectorToOriented, transformOrientedRasterPointToNative, transformOrientedRasterRectToNative, transformOrientedRasterVectorToNative } from "./output/capture-geometry.js";
16
21
  export { calculateIdealApertureGeometry } from "./optics/aperture.js";
17
22
  export { calculateAiryDisk } from "./optics/diffraction.js";
18
23
  export { estimateEquivalentViewingCircleOfConfusion } from "./optics/circle-of-confusion.js";
19
24
  export { calculateThinLensImageDistance } from "./optics/thin-lens.js";
20
25
  export { calculateDefocusCircle, calculateDepthOfField } from "./optics/depth-of-field.js";
26
+ export { calculateFocusBreathingFieldOfView, calculateFocusBreathingProjection } from "./optics/focus-breathing.js";
27
+ export { calculateInverseRadialDistortionMapping, calculateInverseRadialDistortionMappings, calculateRadialDistortionMapping } from "./optics/radial-distortion.js";
28
+ export { calculateInverseLateralChromaticAberrationMapping, calculateInverseLateralChromaticAberrationMappings, calculateLateralChromaticAberrationMapping } from "./optics/lateral-chromatic-aberration.js";
29
+ export { calculateIlluminationVignetting } from "./optics/illumination-vignetting.js";
30
+ export { PSF_FOUNDATION_VERSION, calculatePsfFoundationComponents, getPsfFoundationContract } from "./optics/psf-foundation.js";
21
31
  export { calculateProjectedMotionBlur } from "./motion/projected-motion.js";
32
+ export { calculateCameraRotationImageMapping } from "./motion/camera-rotation.js";
22
33
  export { calculateEquivalentIso, calculateExposureValue100, calculateRelativeOpticalExposure, calculateRelativeRenderedExposure } from "./exposure/exposure.js";
23
34
  export { estimateCameraShakeBlur } from "./stabilization/camera-shake.js";
24
35
  export { calculatePhotoelectrons, calculateSignalToNoise } from "./sensor/signal-noise.js";
25
- export { simulatePocCamera } from "./simulation/poc-simulation.js";
36
+ export { assessRadiometryReadiness, parseRadiometryReadinessProfile } from "./sensor/radiometry-readiness.js";
37
+ export { POC_SIMULATION_API_VERSION, simulatePocCamera } from "./simulation/poc-simulation.js";
26
38
  export { InvalidConfigurationError, parseCameraConfiguration, parseSceneDefinition } from "./schema/validation.js";
27
39
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC;AAEpD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,0BAA0B,EAW3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,oBAAoB,EAGrB,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,4BAA4B,EAG7B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,mBAAmB,EAGpB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,qBAAqB,EAGtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,2BAA2B,EAG5B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,8BAA8B,EAI/B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,iBAAiB,EAGlB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,0CAA0C,EAG3C,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,8BAA8B,EAG/B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EAKtB,MAAM,4BAA4B,CAAC;AAqBpC,OAAO,EACL,4BAA4B,EAG7B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,gCAAgC,EAChC,iCAAiC,EAOlC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,uBAAuB,EAIxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EAIvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,iBAAiB,EAGlB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,QAAiB,CAAC;AAEpD,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,0BAA0B,EAW3B,MAAM,8BAA8B,CAAC;AACtC,OAAO,EACL,2BAA2B,EAC3B,4BAA4B,EAC7B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,gCAAgC,EAChC,yBAAyB,EAY1B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,oBAAoB,EACpB,0BAA0B,EAK3B,MAAM,2BAA2B,CAAC;AAEnC,OAAO,EACL,4BAA4B,EAG7B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EACL,kCAAkC,EAGnC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,mBAAmB,EAGpB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,2BAA2B,EAC3B,8BAA8B,EAO/B,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,8BAA8B,EAU/B,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,qBAAqB,EAGtB,MAAM,kBAAkB,CAAC;AAE1B,OAAO,EACL,2BAA2B,EAG5B,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,iCAAiC,EACjC,0BAA0B,EAC1B,sCAAsC,EACtC,sCAAsC,EACtC,sBAAsB,EACtB,oCAAoC,EACpC,mCAAmC,EACnC,qCAAqC,EACrC,oCAAoC,EACpC,mCAAmC,EACnC,qCAAqC,EAmBtC,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,8BAA8B,EAI/B,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EACL,iBAAiB,EAGlB,MAAM,yBAAyB,CAAC;AAEjC,OAAO,EACL,0CAA0C,EAG3C,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,8BAA8B,EAG/B,MAAM,uBAAuB,CAAC;AAE/B,OAAO,EACL,sBAAsB,EACtB,qBAAqB,EAKtB,MAAM,4BAA4B,CAAC;AAEpC,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EAKlC,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,uCAAuC,EACvC,wCAAwC,EACxC,gCAAgC,EAUjC,MAAM,+BAA+B,CAAC;AAEvC,OAAO,EACL,iDAAiD,EACjD,kDAAkD,EAClD,0CAA0C,EAa3C,MAAM,0CAA0C,CAAC;AAElD,OAAO,EACL,+BAA+B,EAKhC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,EACL,sBAAsB,EACtB,gCAAgC,EAChC,wBAAwB,EAOzB,MAAM,4BAA4B,CAAC;AAqBpC,OAAO,EACL,4BAA4B,EAG7B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,mCAAmC,EAMpC,MAAM,6BAA6B,CAAC;AAErC,OAAO,EACL,sBAAsB,EACtB,yBAAyB,EACzB,gCAAgC,EAChC,iCAAiC,EAOlC,MAAM,wBAAwB,CAAC;AAEhC,OAAO,EACL,uBAAuB,EAIxB,MAAM,iCAAiC,CAAC;AAEzC,OAAO,EACL,uBAAuB,EACvB,sBAAsB,EAIvB,MAAM,0BAA0B,CAAC;AAElC,OAAO,EACL,yBAAyB,EACzB,+BAA+B,EAchC,MAAM,kCAAkC,CAAC;AAE1C,OAAO,EACL,0BAA0B,EAC1B,iBAAiB,EAGlB,MAAM,gCAAgC,CAAC;AAExC,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACrB,MAAM,wBAAwB,CAAC"}
@@ -0,0 +1,91 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CameraAngularVelocityRadPerSec {
3
+ /** Right-hand rotation rate about camera +X (right), in radians per second. */
4
+ pitch: number;
5
+ /** Right-hand rotation rate about camera +Y (up), in radians per second. */
6
+ yaw: number;
7
+ /** Right-hand rotation rate about camera +Z (forward), in radians per second. */
8
+ roll: number;
9
+ }
10
+ export interface ImagePlanePointMm {
11
+ /** Image-plane X coordinate relative to the optical axis, in millimetres. */
12
+ x: number;
13
+ /** Image-plane Y coordinate relative to the optical axis, in millimetres. */
14
+ y: number;
15
+ }
16
+ export interface AxisSamplingPitchMicrometers {
17
+ x: number;
18
+ y: number;
19
+ }
20
+ export interface CalculateCameraRotationImageMappingInput {
21
+ /** Physical focal length in millimetres. */
22
+ focalLengthMm: number;
23
+ /**
24
+ * Stationary-world-ray image-plane position at exposure start.
25
+ *
26
+ * Image-plane coordinates use optical-axis origin, +X right, +Y up.
27
+ */
28
+ imagePointMm: ImagePlanePointMm;
29
+ /**
30
+ * Time in seconds from exposure start. Must be greater than or equal to zero.
31
+ */
32
+ timeSecondsFromExposureStart: number;
33
+ /**
34
+ * Constant camera angular-velocity vector in radians per second.
35
+ *
36
+ * Components are resolved in the camera axes at exposure start and follow
37
+ * the right-hand rule: pitch about +X, yaw about +Y, roll about +Z.
38
+ */
39
+ angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;
40
+ /** Optional focus distance in metres for thin-lens projection distance. */
41
+ focusDistanceM?: number;
42
+ /**
43
+ * Optional axis-aware geometric sampling pitch for reporting sample-domain
44
+ * displacement. This is geometric sample spacing, not photosite collection
45
+ * area.
46
+ */
47
+ samplingPitchMicrometers?: AxisSamplingPitchMicrometers;
48
+ }
49
+ export interface CameraRotationImageMapping {
50
+ projectionDistanceMm: number;
51
+ timeSecondsFromExposureStart: number;
52
+ angularDisplacementRad: {
53
+ pitch: number;
54
+ yaw: number;
55
+ roll: number;
56
+ magnitude: number;
57
+ };
58
+ startImagePointMm: ImagePlanePointMm;
59
+ mappedImagePointMm: ImagePlanePointMm;
60
+ deltaMm: {
61
+ x: number;
62
+ y: number;
63
+ distance: number;
64
+ };
65
+ /**
66
+ * Optional geometric sample displacement in the image-plane basis
67
+ * (+X right, +Y up). This is not a native-raster vector.
68
+ */
69
+ deltaImagePlaneSamples?: {
70
+ x: number;
71
+ y: number;
72
+ distance: number;
73
+ };
74
+ }
75
+ /**
76
+ * Maps one stationary-world ray to its image-plane location after pure camera
77
+ * rotation at an arbitrary physical time from exposure start.
78
+ *
79
+ * The initial image point defines the world ray in the camera frame at t=0.
80
+ * Constant angular velocity is integrated as one axis-angle rotation vector,
81
+ * avoiding Euler-order dependence. The stationary world ray is then expressed
82
+ * in the rotated camera frame using the inverse camera rotation and projected
83
+ * rectilinearly back onto the image plane.
84
+ *
85
+ * This function models rotation only. It deliberately excludes camera
86
+ * translation because translational optical flow depends on scene depth.
87
+ *
88
+ * @returns Field-position-dependent image-plane mapping and displacement.
89
+ */
90
+ export declare function calculateCameraRotationImageMapping(input: CalculateCameraRotationImageMappingInput): CalculationResult<CameraRotationImageMapping>;
91
+ //# sourceMappingURL=camera-rotation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-rotation.d.ts","sourceRoot":"","sources":["../../src/motion/camera-rotation.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAOvC,MAAM,WAAW,8BAA8B;IAC7C,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd,4EAA4E;IAC5E,GAAG,EAAE,MAAM,CAAC;IACZ,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,CAAC,EAAE,MAAM,CAAC;IACV,6EAA6E;IAC7E,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,4BAA4B;IAC3C,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,wCAAwC;IACvD,4CAA4C;IAC5C,aAAa,EAAE,MAAM,CAAC;IACtB;;;;OAIG;IACH,YAAY,EAAE,iBAAiB,CAAC;IAChC;;OAEG;IACH,4BAA4B,EAAE,MAAM,CAAC;IACrC;;;;;OAKG;IACH,wBAAwB,EAAE,8BAA8B,CAAC;IACzD,2EAA2E;IAC3E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,wBAAwB,CAAC,EAAE,4BAA4B,CAAC;CACzD;AAED,MAAM,WAAW,0BAA0B;IACzC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,4BAA4B,EAAE,MAAM,CAAC;IACrC,sBAAsB,EAAE;QACtB,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,kBAAkB,EAAE,iBAAiB,CAAC;IACtC,OAAO,EAAE;QACP,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF;;;OAGG;IACH,sBAAsB,CAAC,EAAE;QACvB,CAAC,EAAE,MAAM,CAAC;QACV,CAAC,EAAE,MAAM,CAAC;QACV,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAkFD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,mCAAmC,CACjD,KAAK,EAAE,wCAAwC,GAC9C,iBAAiB,CAAC,0BAA0B,CAAC,CAkJ/C"}
@@ -0,0 +1,157 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { calculatedResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
4
+ import { calculateThinLensImageDistance } from "../optics/thin-lens.js";
5
+ function requireFinite(name, value) {
6
+ if (!Number.isFinite(value)) {
7
+ throw new InvalidScientificInputError(`${name} must be finite.`);
8
+ }
9
+ }
10
+ function requireNonNegativeFinite(name, value) {
11
+ if (!Number.isFinite(value) || value < 0) {
12
+ throw new InvalidScientificInputError(`${name} must be finite and greater than or equal to zero.`);
13
+ }
14
+ }
15
+ function rotateVectorByAxisAngle(vector, axis, angleRad) {
16
+ if (angleRad === 0) {
17
+ return { ...vector };
18
+ }
19
+ const cosine = Math.cos(angleRad);
20
+ const sine = Math.sin(angleRad);
21
+ const oneMinusCosine = 1 - cosine;
22
+ const dot = axis.x * vector.x +
23
+ axis.y * vector.y +
24
+ axis.z * vector.z;
25
+ const cross = {
26
+ x: axis.y * vector.z - axis.z * vector.y,
27
+ y: axis.z * vector.x - axis.x * vector.z,
28
+ z: axis.x * vector.y - axis.y * vector.x
29
+ };
30
+ return {
31
+ x: vector.x * cosine +
32
+ cross.x * sine +
33
+ axis.x * dot * oneMinusCosine,
34
+ y: vector.y * cosine +
35
+ cross.y * sine +
36
+ axis.y * dot * oneMinusCosine,
37
+ z: vector.z * cosine +
38
+ cross.z * sine +
39
+ axis.z * dot * oneMinusCosine
40
+ };
41
+ }
42
+ function resolveProjectionDistanceMm(focalLengthMm, focusDistanceM) {
43
+ const projection = focusDistanceM === undefined
44
+ ? undefined
45
+ : calculateThinLensImageDistance({
46
+ focalLengthMm,
47
+ objectDistanceM: focusDistanceM
48
+ });
49
+ return {
50
+ projectionDistanceMm: projection?.value.imageDistanceMm ?? focalLengthMm,
51
+ focusAware: projection !== undefined
52
+ };
53
+ }
54
+ /**
55
+ * Maps one stationary-world ray to its image-plane location after pure camera
56
+ * rotation at an arbitrary physical time from exposure start.
57
+ *
58
+ * The initial image point defines the world ray in the camera frame at t=0.
59
+ * Constant angular velocity is integrated as one axis-angle rotation vector,
60
+ * avoiding Euler-order dependence. The stationary world ray is then expressed
61
+ * in the rotated camera frame using the inverse camera rotation and projected
62
+ * rectilinearly back onto the image plane.
63
+ *
64
+ * This function models rotation only. It deliberately excludes camera
65
+ * translation because translational optical flow depends on scene depth.
66
+ *
67
+ * @returns Field-position-dependent image-plane mapping and displacement.
68
+ */
69
+ export function calculateCameraRotationImageMapping(input) {
70
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
71
+ requireFinite("imagePointMm.x", input.imagePointMm.x);
72
+ requireFinite("imagePointMm.y", input.imagePointMm.y);
73
+ requireNonNegativeFinite("timeSecondsFromExposureStart", input.timeSecondsFromExposureStart);
74
+ requireFinite("angularVelocityRadPerSec.pitch", input.angularVelocityRadPerSec.pitch);
75
+ requireFinite("angularVelocityRadPerSec.yaw", input.angularVelocityRadPerSec.yaw);
76
+ requireFinite("angularVelocityRadPerSec.roll", input.angularVelocityRadPerSec.roll);
77
+ if (input.samplingPitchMicrometers !== undefined) {
78
+ requirePositiveFinite("samplingPitchMicrometers.x", input.samplingPitchMicrometers.x);
79
+ requirePositiveFinite("samplingPitchMicrometers.y", input.samplingPitchMicrometers.y);
80
+ }
81
+ const { projectionDistanceMm, focusAware } = resolveProjectionDistanceMm(input.focalLengthMm, input.focusDistanceM);
82
+ const angularDisplacementRad = {
83
+ pitch: input.angularVelocityRadPerSec.pitch *
84
+ input.timeSecondsFromExposureStart,
85
+ yaw: input.angularVelocityRadPerSec.yaw *
86
+ input.timeSecondsFromExposureStart,
87
+ roll: input.angularVelocityRadPerSec.roll *
88
+ input.timeSecondsFromExposureStart
89
+ };
90
+ const angularMagnitude = Math.hypot(angularDisplacementRad.pitch, angularDisplacementRad.yaw, angularDisplacementRad.roll);
91
+ const initialRay = {
92
+ x: input.imagePointMm.x / projectionDistanceMm,
93
+ y: input.imagePointMm.y / projectionDistanceMm,
94
+ z: 1
95
+ };
96
+ let mappedRay = initialRay;
97
+ if (angularMagnitude !== 0) {
98
+ const inverseAxis = {
99
+ x: angularDisplacementRad.pitch / angularMagnitude,
100
+ y: angularDisplacementRad.yaw / angularMagnitude,
101
+ z: angularDisplacementRad.roll / angularMagnitude
102
+ };
103
+ mappedRay = rotateVectorByAxisAngle(initialRay, inverseAxis, -angularMagnitude);
104
+ }
105
+ if (!Number.isFinite(mappedRay.z) || mappedRay.z <= 0) {
106
+ throw new InvalidScientificInputError("Camera rotation maps the requested world ray to or behind the camera plane.");
107
+ }
108
+ const mappedImagePointMm = {
109
+ x: projectionDistanceMm * (mappedRay.x / mappedRay.z),
110
+ y: projectionDistanceMm * (mappedRay.y / mappedRay.z)
111
+ };
112
+ const deltaMm = {
113
+ x: mappedImagePointMm.x - input.imagePointMm.x,
114
+ y: mappedImagePointMm.y - input.imagePointMm.y,
115
+ distance: Math.hypot(mappedImagePointMm.x - input.imagePointMm.x, mappedImagePointMm.y - input.imagePointMm.y)
116
+ };
117
+ let deltaImagePlaneSamples;
118
+ if (input.samplingPitchMicrometers !== undefined) {
119
+ const pitchXmm = input.samplingPitchMicrometers.x / 1000;
120
+ const pitchYmm = input.samplingPitchMicrometers.y / 1000;
121
+ const sampleX = deltaMm.x / pitchXmm;
122
+ const sampleY = deltaMm.y / pitchYmm;
123
+ deltaImagePlaneSamples = {
124
+ x: sampleX,
125
+ y: sampleY,
126
+ distance: Math.hypot(sampleX, sampleY)
127
+ };
128
+ }
129
+ return calculatedResult({
130
+ projectionDistanceMm,
131
+ timeSecondsFromExposureStart: input.timeSecondsFromExposureStart,
132
+ angularDisplacementRad: {
133
+ ...angularDisplacementRad,
134
+ magnitude: angularMagnitude
135
+ },
136
+ startImagePointMm: { ...input.imagePointMm },
137
+ mappedImagePointMm,
138
+ deltaMm,
139
+ ...(deltaImagePlaneSamples === undefined
140
+ ? {}
141
+ : { deltaImagePlaneSamples })
142
+ }, focusAware
143
+ ? "focus-aware-spatial-camera-rotation-mapping"
144
+ : "spatial-camera-rotation-mapping", "1.0.0", [
145
+ "Ideal rectilinear projection",
146
+ focusAware
147
+ ? "Thin-lens image distance for the selected focus plane is used as the projection distance"
148
+ : "Nominal focal length is used as the infinity-focus projection distance",
149
+ "Camera angular velocity is constant during the evaluated interval",
150
+ "Angular velocity components are resolved in the camera axes at exposure start and integrated as one axis-angle vector",
151
+ "Positive pitch/yaw/roll follow the right-hand rule about camera +X/+Y/+Z",
152
+ "The world ray is stationary; subject motion is not included",
153
+ "Camera translation and depth-dependent parallax are not modeled",
154
+ "The stationary world ray is transformed by the inverse camera rotation before rectilinear projection"
155
+ ]);
156
+ }
157
+ //# sourceMappingURL=camera-rotation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"camera-rotation.js","sourceRoot":"","sources":["../../src/motion/camera-rotation.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,73 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ export interface CalculateFocusBreathingProjectionInput {
3
+ /** Physical lens focal length in millimetres. This value is never rewritten. */
4
+ focalLengthMm: number;
5
+ /** Selected focus distance in metres. */
6
+ focusDistanceM: number;
7
+ /**
8
+ * Caller-declared projection scale for this focus state, relative to the
9
+ * ideal thin-lens image distance.
10
+ *
11
+ * 1 means no additional breathing relative to the current thin-lens model.
12
+ * Values above 1 narrow framing / increase magnification; values below 1
13
+ * widen framing / reduce magnification.
14
+ */
15
+ breathingProjectionScale: number;
16
+ }
17
+ export interface FocusBreathingProjection {
18
+ /** Authoritative physical focal length supplied by the caller. */
19
+ physicalFocalLengthMm: number;
20
+ focusDistanceM: number;
21
+ /** Ideal Gaussian thin-lens image distance before breathing adjustment. */
22
+ idealProjectionDistanceMm: number;
23
+ /**
24
+ * Effective projection distance used by the generic breathing model.
25
+ *
26
+ * This is a projection/magnification parameter, not a replacement physical
27
+ * focal length.
28
+ */
29
+ effectiveProjectionDistanceMm: number;
30
+ /** Caller-declared scale relative to ideal thin-lens projection. */
31
+ breathingProjectionScale: number;
32
+ /** Ideal Gaussian thin-lens magnification at the selected focus distance. */
33
+ idealMagnification: number;
34
+ /** Generic breathing-adjusted magnification. */
35
+ effectiveMagnification: number;
36
+ }
37
+ export interface CalculateFocusBreathingFieldOfViewInput extends CalculateFocusBreathingProjectionInput {
38
+ /** Sensor/capture dimension corresponding to the requested FOV, in millimetres. */
39
+ sensorDimensionMm: number;
40
+ }
41
+ export interface FocusBreathingFieldOfView {
42
+ physicalFocalLengthMm: number;
43
+ focusDistanceM: number;
44
+ sensorDimensionMm: number;
45
+ breathingProjectionScale: number;
46
+ idealProjectionDistanceMm: number;
47
+ effectiveProjectionDistanceMm: number;
48
+ idealDegrees: number;
49
+ effectiveDegrees: number;
50
+ deltaDegrees: number;
51
+ }
52
+ /**
53
+ * Applies a caller-declared focus-breathing projection scale to the ideal
54
+ * Gaussian thin-lens projection for one focus state.
55
+ *
56
+ * Photivra does not infer the scale from focal length, focus distance, lens
57
+ * identity, or marketing data. A value of 1 exactly preserves the current
58
+ * thin-lens projection.
59
+ *
60
+ * This is a generic approximation of focus-dependent projection/magnification,
61
+ * not a calibrated real-lens model. The physical focal length remains
62
+ * authoritative and unchanged.
63
+ */
64
+ export declare function calculateFocusBreathingProjection(input: CalculateFocusBreathingProjectionInput): CalculationResult<FocusBreathingProjection>;
65
+ /**
66
+ * Calculates one-axis field of view for the generic declared-scale focus-
67
+ * breathing projection and reports the corresponding ideal thin-lens FOV.
68
+ *
69
+ * A breathingProjectionScale of 1 reproduces the current focus-aware
70
+ * thin-lens field-of-view result exactly.
71
+ */
72
+ export declare function calculateFocusBreathingFieldOfView(input: CalculateFocusBreathingFieldOfViewInput): CalculationResult<FocusBreathingFieldOfView>;
73
+ //# sourceMappingURL=focus-breathing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-breathing.d.ts","sourceRoot":"","sources":["../../src/optics/focus-breathing.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAIvC,MAAM,WAAW,sCAAsC;IACrD,gFAAgF;IAChF,aAAa,EAAE,MAAM,CAAC;IACtB,yCAAyC;IACzC,cAAc,EAAE,MAAM,CAAC;IACvB;;;;;;;OAOG;IACH,wBAAwB,EAAE,MAAM,CAAC;CAClC;AAED,MAAM,WAAW,wBAAwB;IACvC,kEAAkE;IAClE,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,2EAA2E;IAC3E,yBAAyB,EAAE,MAAM,CAAC;IAClC;;;;;OAKG;IACH,6BAA6B,EAAE,MAAM,CAAC;IACtC,oEAAoE;IACpE,wBAAwB,EAAE,MAAM,CAAC;IACjC,6EAA6E;IAC7E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gDAAgD;IAChD,sBAAsB,EAAE,MAAM,CAAC;CAChC;AAED,MAAM,WAAW,uCACf,SAAQ,sCAAsC;IAC9C,mFAAmF;IACnF,iBAAiB,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,yBAAyB;IACxC,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,MAAM,CAAC;IACvB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,wBAAwB,EAAE,MAAM,CAAC;IACjC,yBAAyB,EAAE,MAAM,CAAC;IAClC,6BAA6B,EAAE,MAAM,CAAC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,gBAAgB,EAAE,MAAM,CAAC;IACzB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,iCAAiC,CAC/C,KAAK,EAAE,sCAAsC,GAC5C,iBAAiB,CAAC,wBAAwB,CAAC,CAsC7C;AAED;;;;;;GAMG;AACH,wBAAgB,kCAAkC,CAChD,KAAK,EAAE,uCAAuC,GAC7C,iBAAiB,CAAC,yBAAyB,CAAC,CAwC9C"}
@@ -0,0 +1,77 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { approximationResult } from "../core/calculation-result.js";
3
+ import { requirePositiveFinite } from "../core/validation.js";
4
+ import { calculateThinLensImageDistance } from "./thin-lens.js";
5
+ /**
6
+ * Applies a caller-declared focus-breathing projection scale to the ideal
7
+ * Gaussian thin-lens projection for one focus state.
8
+ *
9
+ * Photivra does not infer the scale from focal length, focus distance, lens
10
+ * identity, or marketing data. A value of 1 exactly preserves the current
11
+ * thin-lens projection.
12
+ *
13
+ * This is a generic approximation of focus-dependent projection/magnification,
14
+ * not a calibrated real-lens model. The physical focal length remains
15
+ * authoritative and unchanged.
16
+ */
17
+ export function calculateFocusBreathingProjection(input) {
18
+ requirePositiveFinite("focalLengthMm", input.focalLengthMm);
19
+ requirePositiveFinite("focusDistanceM", input.focusDistanceM);
20
+ requirePositiveFinite("breathingProjectionScale", input.breathingProjectionScale);
21
+ const thinLens = calculateThinLensImageDistance({
22
+ focalLengthMm: input.focalLengthMm,
23
+ objectDistanceM: input.focusDistanceM
24
+ });
25
+ const idealProjectionDistanceMm = thinLens.value.imageDistanceMm;
26
+ const effectiveProjectionDistanceMm = idealProjectionDistanceMm * input.breathingProjectionScale;
27
+ const objectDistanceMm = input.focusDistanceM * 1000;
28
+ return approximationResult({
29
+ physicalFocalLengthMm: input.focalLengthMm,
30
+ focusDistanceM: input.focusDistanceM,
31
+ idealProjectionDistanceMm,
32
+ effectiveProjectionDistanceMm,
33
+ breathingProjectionScale: input.breathingProjectionScale,
34
+ idealMagnification: thinLens.value.magnification,
35
+ effectiveMagnification: effectiveProjectionDistanceMm / objectDistanceMm
36
+ }, "declared-scale-focus-breathing-projection", "1.0.0", [
37
+ "Base projection is the ideal Gaussian thin-lens image distance for the selected focus distance",
38
+ "The focus-breathing projection scale is supplied explicitly by the caller and is not inferred",
39
+ "One declared scale applies uniformly to the current focus state",
40
+ "Physical focal length remains unchanged and authoritative",
41
+ "The model represents focus-dependent framing/magnification only; distortion, pupil magnification, aberrations, and named-lens calibration are not modeled"
42
+ ]);
43
+ }
44
+ /**
45
+ * Calculates one-axis field of view for the generic declared-scale focus-
46
+ * breathing projection and reports the corresponding ideal thin-lens FOV.
47
+ *
48
+ * A breathingProjectionScale of 1 reproduces the current focus-aware
49
+ * thin-lens field-of-view result exactly.
50
+ */
51
+ export function calculateFocusBreathingFieldOfView(input) {
52
+ requirePositiveFinite("sensorDimensionMm", input.sensorDimensionMm);
53
+ const projection = calculateFocusBreathingProjection(input);
54
+ const idealRadians = 2 *
55
+ Math.atan(input.sensorDimensionMm /
56
+ (2 * projection.value.idealProjectionDistanceMm));
57
+ const effectiveRadians = 2 *
58
+ Math.atan(input.sensorDimensionMm /
59
+ (2 * projection.value.effectiveProjectionDistanceMm));
60
+ const idealDegrees = (idealRadians * 180) / Math.PI;
61
+ const effectiveDegrees = (effectiveRadians * 180) / Math.PI;
62
+ return approximationResult({
63
+ physicalFocalLengthMm: input.focalLengthMm,
64
+ focusDistanceM: input.focusDistanceM,
65
+ sensorDimensionMm: input.sensorDimensionMm,
66
+ breathingProjectionScale: input.breathingProjectionScale,
67
+ idealProjectionDistanceMm: projection.value.idealProjectionDistanceMm,
68
+ effectiveProjectionDistanceMm: projection.value.effectiveProjectionDistanceMm,
69
+ idealDegrees,
70
+ effectiveDegrees,
71
+ deltaDegrees: effectiveDegrees - idealDegrees
72
+ }, "declared-scale-focus-breathing-field-of-view", "1.0.0", [
73
+ ...(projection.provenance.assumptions ?? []),
74
+ "Rectilinear field of view is calculated from the declared sensor/capture dimension and effective projection distance"
75
+ ]);
76
+ }
77
+ //# sourceMappingURL=focus-breathing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"focus-breathing.js","sourceRoot":"","sources":["../../src/optics/focus-breathing.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,mBAAmB,EAEpB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC9D,OAAO,EAAE,8BAA8B,EAAE,MAAM,gBAAgB,CAAC;AAyDhE;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,iCAAiC,CAC/C,KAA6C;IAE7C,qBAAqB,CAAC,eAAe,EAAE,KAAK,CAAC,aAAa,CAAC,CAAC;IAC5D,qBAAqB,CAAC,gBAAgB,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9D,qBAAqB,CACnB,0BAA0B,EAC1B,KAAK,CAAC,wBAAwB,CAC/B,CAAC;IAEF,MAAM,QAAQ,GAAG,8BAA8B,CAAC;QAC9C,aAAa,EAAE,KAAK,CAAC,aAAa;QAClC,eAAe,EAAE,KAAK,CAAC,cAAc;KACtC,CAAC,CAAC;IACH,MAAM,yBAAyB,GAAG,QAAQ,CAAC,KAAK,CAAC,eAAe,CAAC;IACjE,MAAM,6BAA6B,GACjC,yBAAyB,GAAG,KAAK,CAAC,wBAAwB,CAAC;IAC7D,MAAM,gBAAgB,GAAG,KAAK,CAAC,cAAc,GAAG,IAAI,CAAC;IAErD,OAAO,mBAAmB,CACxB;QACE,qBAAqB,EAAE,KAAK,CAAC,aAAa;QAC1C,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,yBAAyB;QACzB,6BAA6B;QAC7B,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QACxD,kBAAkB,EAAE,QAAQ,CAAC,KAAK,CAAC,aAAa;QAChD,sBAAsB,EACpB,6BAA6B,GAAG,gBAAgB;KACnD,EACD,2CAA2C,EAC3C,OAAO,EACP;QACE,gGAAgG;QAChG,+FAA+F;QAC/F,iEAAiE;QACjE,2DAA2D;QAC3D,2JAA2J;KAC5J,CACF,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,kCAAkC,CAChD,KAA8C;IAE9C,qBAAqB,CAAC,mBAAmB,EAAE,KAAK,CAAC,iBAAiB,CAAC,CAAC;IAEpE,MAAM,UAAU,GAAG,iCAAiC,CAAC,KAAK,CAAC,CAAC;IAC5D,MAAM,YAAY,GAChB,CAAC;QACD,IAAI,CAAC,IAAI,CACP,KAAK,CAAC,iBAAiB;YACrB,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,yBAAyB,CAAC,CACnD,CAAC;IACJ,MAAM,gBAAgB,GACpB,CAAC;QACD,IAAI,CAAC,IAAI,CACP,KAAK,CAAC,iBAAiB;YACrB,CAAC,CAAC,GAAG,UAAU,CAAC,KAAK,CAAC,6BAA6B,CAAC,CACvD,CAAC;IACJ,MAAM,YAAY,GAAG,CAAC,YAAY,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IACpD,MAAM,gBAAgB,GAAG,CAAC,gBAAgB,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,EAAE,CAAC;IAE5D,OAAO,mBAAmB,CACxB;QACE,qBAAqB,EAAE,KAAK,CAAC,aAAa;QAC1C,cAAc,EAAE,KAAK,CAAC,cAAc;QACpC,iBAAiB,EAAE,KAAK,CAAC,iBAAiB;QAC1C,wBAAwB,EAAE,KAAK,CAAC,wBAAwB;QACxD,yBAAyB,EACvB,UAAU,CAAC,KAAK,CAAC,yBAAyB;QAC5C,6BAA6B,EAC3B,UAAU,CAAC,KAAK,CAAC,6BAA6B;QAChD,YAAY;QACZ,gBAAgB;QAChB,YAAY,EAAE,gBAAgB,GAAG,YAAY;KAC9C,EACD,8CAA8C,EAC9C,OAAO,EACP;QACE,GAAG,CAAC,UAAU,CAAC,UAAU,CAAC,WAAW,IAAI,EAAE,CAAC;QAC5C,sHAAsH;KACvH,CACF,CAAC;AACJ,CAAC"}
@@ -0,0 +1,59 @@
1
+ import { type CalculationResult } from "../core/calculation-result.js";
2
+ import type { LensFieldPointMm } from "./radial-distortion.js";
3
+ export interface IlluminationVignettingCoefficients {
4
+ /** Coefficient on normalized radius squared. */
5
+ r2: number;
6
+ /** Coefficient on normalized radius to the fourth power. */
7
+ r4: number;
8
+ /** Coefficient on normalized radius to the sixth power. */
9
+ r6: number;
10
+ }
11
+ export interface IlluminationVignettingProfile {
12
+ /** Physical image-plane radius used to normalize field position, in mm. */
13
+ normalizationRadiusMm: number;
14
+ /** Maximum normalized image-plane radius over which the profile is valid. */
15
+ maximumNormalizedRadius: number;
16
+ /**
17
+ * Generic radial relative-illumination coefficients.
18
+ *
19
+ * Relative linear throughput is:
20
+ * 1 + r2 * rho^2 + r4 * rho^4 + r6 * rho^6
21
+ *
22
+ * where rho is physical image-plane radius / normalizationRadiusMm.
23
+ */
24
+ coefficients: IlluminationVignettingCoefficients;
25
+ }
26
+ export interface CalculateIlluminationVignettingInput {
27
+ /**
28
+ * Ideal image-plane field position relative to the optical axis, in mm.
29
+ *
30
+ * This slice evaluates throughput from the declared ideal field coordinate;
31
+ * it does not apply or invert geometric distortion.
32
+ */
33
+ imagePointMm: LensFieldPointMm;
34
+ profile: IlluminationVignettingProfile;
35
+ }
36
+ export interface IlluminationVignetting {
37
+ imagePointMm: LensFieldPointMm;
38
+ normalizedRadius: number;
39
+ /**
40
+ * Multiplicative factor for scene-linear/channel-linear signal.
41
+ *
42
+ * 1 means no attenuation; values are required to remain in (0, 1].
43
+ */
44
+ linearThroughputFactor: number;
45
+ /** Positive exposure loss relative to the optical axis, in stops. */
46
+ attenuationStops: number;
47
+ profileMinimumThroughputFactor: number;
48
+ profileMaximumThroughputFactor: number;
49
+ }
50
+ /**
51
+ * Calculates generic field-dependent illumination falloff as a multiplicative
52
+ * linear-light throughput factor.
53
+ *
54
+ * The profile is accepted only when its relative throughput remains strictly
55
+ * positive and never exceeds the optical-axis normalization of 1 throughout
56
+ * the complete declared operating envelope.
57
+ */
58
+ export declare function calculateIlluminationVignetting(input: CalculateIlluminationVignettingInput): CalculationResult<IlluminationVignetting>;
59
+ //# sourceMappingURL=illumination-vignetting.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"illumination-vignetting.d.ts","sourceRoot":"","sources":["../../src/optics/illumination-vignetting.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,iBAAiB,EACvB,MAAM,+BAA+B,CAAC;AAKvC,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAE/D,MAAM,WAAW,kCAAkC;IACjD,gDAAgD;IAChD,EAAE,EAAE,MAAM,CAAC;IACX,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAC;IACX,2DAA2D;IAC3D,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,WAAW,6BAA6B;IAC5C,2EAA2E;IAC3E,qBAAqB,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,uBAAuB,EAAE,MAAM,CAAC;IAChC;;;;;;;OAOG;IACH,YAAY,EAAE,kCAAkC,CAAC;CAClD;AAED,MAAM,WAAW,oCAAoC;IACnD;;;;;OAKG;IACH,YAAY,EAAE,gBAAgB,CAAC;IAC/B,OAAO,EAAE,6BAA6B,CAAC;CACxC;AAED,MAAM,WAAW,sBAAsB;IACrC,YAAY,EAAE,gBAAgB,CAAC;IAC/B,gBAAgB,EAAE,MAAM,CAAC;IACzB;;;;OAIG;IACH,sBAAsB,EAAE,MAAM,CAAC;IAC/B,qEAAqE;IACrE,gBAAgB,EAAE,MAAM,CAAC;IACzB,8BAA8B,EAAE,MAAM,CAAC;IACvC,8BAA8B,EAAE,MAAM,CAAC;CACxC;AA0HD;;;;;;;GAOG;AACH,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,oCAAoC,GAC1C,iBAAiB,CAAC,sBAAsB,CAAC,CAsC3C"}
@@ -0,0 +1,107 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { approximationResult } from "../core/calculation-result.js";
3
+ import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
4
+ function requireFinite(name, value) {
5
+ if (!Number.isFinite(value)) {
6
+ throw new InvalidScientificInputError(`${name} must be finite.`);
7
+ }
8
+ }
9
+ function throughputFromSquaredRadius(squaredRadius, coefficients) {
10
+ return (1 +
11
+ coefficients.r2 * squaredRadius +
12
+ coefficients.r4 * squaredRadius * squaredRadius +
13
+ coefficients.r6 *
14
+ squaredRadius *
15
+ squaredRadius *
16
+ squaredRadius);
17
+ }
18
+ function throughputCriticalSquaredRadii(coefficients) {
19
+ // For s = rho²:
20
+ // T(s) = 1 + r2*s + r4*s² + r6*s³
21
+ // dT/ds = r2 + 2*r4*s + 3*r6*s²
22
+ const a = 3 * coefficients.r6;
23
+ const b = 2 * coefficients.r4;
24
+ const c = coefficients.r2;
25
+ if (a === 0) {
26
+ return b === 0 ? [] : [-c / b];
27
+ }
28
+ const discriminant = b * b - 4 * a * c;
29
+ if (discriminant < 0) {
30
+ return [];
31
+ }
32
+ const root = Math.sqrt(discriminant);
33
+ return [(-b - root) / (2 * a), (-b + root) / (2 * a)];
34
+ }
35
+ function validateProfile(profile) {
36
+ requirePositiveFinite("profile.normalizationRadiusMm", profile.normalizationRadiusMm);
37
+ requirePositiveFinite("profile.maximumNormalizedRadius", profile.maximumNormalizedRadius);
38
+ requireFinite("profile.coefficients.r2", profile.coefficients.r2);
39
+ requireFinite("profile.coefficients.r4", profile.coefficients.r4);
40
+ requireFinite("profile.coefficients.r6", profile.coefficients.r6);
41
+ const maximumSquaredRadius = profile.maximumNormalizedRadius * profile.maximumNormalizedRadius;
42
+ const candidates = [
43
+ 0,
44
+ maximumSquaredRadius,
45
+ ...throughputCriticalSquaredRadii(profile.coefficients).filter((value) => value > 0 && value < maximumSquaredRadius)
46
+ ];
47
+ const throughputs = candidates.map((squaredRadius) => throughputFromSquaredRadius(squaredRadius, profile.coefficients));
48
+ const minimumThroughputFactor = Math.min(...throughputs);
49
+ const maximumThroughputFactor = Math.max(...throughputs);
50
+ if (!Number.isFinite(minimumThroughputFactor) ||
51
+ minimumThroughputFactor <= 0) {
52
+ throw new InvalidScientificInputError("Illumination vignetting profile must keep relative linear throughput strictly greater than zero over maximumNormalizedRadius.");
53
+ }
54
+ if (!Number.isFinite(maximumThroughputFactor) ||
55
+ maximumThroughputFactor > 1) {
56
+ throw new InvalidScientificInputError("Illumination vignetting profile must not amplify relative linear throughput above the optical-axis value of 1.");
57
+ }
58
+ return {
59
+ normalizationRadiusMm: profile.normalizationRadiusMm,
60
+ maximumNormalizedRadius: profile.maximumNormalizedRadius,
61
+ coefficients: { ...profile.coefficients },
62
+ minimumThroughputFactor,
63
+ maximumThroughputFactor
64
+ };
65
+ }
66
+ function assumptions() {
67
+ return [
68
+ "Generic rotationally symmetric illumination-vignetting approximation evaluated from ideal image-plane field radius",
69
+ "Relative linear throughput is normalized to 1 on the optical axis",
70
+ "The model attenuates scene-linear/channel-linear signal only and must be applied before display/gamma encoding",
71
+ "The model changes throughput only; it does not alter field coordinates, pupil shape, PSF shape, bokeh, or focus",
72
+ "Mechanical/pupil vignetting and cat's-eye bokeh are separate pupil/PSF effects",
73
+ "The profile is a generic caller-supplied radial approximation, not calibrated radiometry or a named-lens measurement"
74
+ ];
75
+ }
76
+ /**
77
+ * Calculates generic field-dependent illumination falloff as a multiplicative
78
+ * linear-light throughput factor.
79
+ *
80
+ * The profile is accepted only when its relative throughput remains strictly
81
+ * positive and never exceeds the optical-axis normalization of 1 throughout
82
+ * the complete declared operating envelope.
83
+ */
84
+ export function calculateIlluminationVignetting(input) {
85
+ requireFinite("imagePointMm.x", input.imagePointMm.x);
86
+ requireFinite("imagePointMm.y", input.imagePointMm.y);
87
+ const profile = validateProfile(input.profile);
88
+ const radiusMm = Math.hypot(input.imagePointMm.x, input.imagePointMm.y);
89
+ const normalizedRadius = radiusMm / profile.normalizationRadiusMm;
90
+ if (normalizedRadius > profile.maximumNormalizedRadius) {
91
+ throw new InvalidScientificInputError("imagePointMm lies outside the illumination vignetting profile maximumNormalizedRadius.");
92
+ }
93
+ const squaredRadius = normalizedRadius * normalizedRadius;
94
+ const linearThroughputFactor = throughputFromSquaredRadius(squaredRadius, profile.coefficients);
95
+ const attenuationStops = linearThroughputFactor === 1
96
+ ? 0
97
+ : -Math.log2(linearThroughputFactor);
98
+ return approximationResult({
99
+ imagePointMm: { ...input.imagePointMm },
100
+ normalizedRadius,
101
+ linearThroughputFactor,
102
+ attenuationStops,
103
+ profileMinimumThroughputFactor: profile.minimumThroughputFactor,
104
+ profileMaximumThroughputFactor: profile.maximumThroughputFactor
105
+ }, "generic-radial-illumination-vignetting", "1.0.0", assumptions());
106
+ }
107
+ //# sourceMappingURL=illumination-vignetting.js.map