@photivra/engine 2.9.0 → 2.10.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 (72) hide show
  1. package/CHANGELOG.md +85 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +6 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +2 -0
  6. package/dist/capture/native-environment-lean.js +89 -5
  7. package/dist/capture/native-environment-lean.js.map +1 -1
  8. package/dist/capture/native-environment-tabulated.d.ts +37 -0
  9. package/dist/capture/native-environment-tabulated.js +42 -6
  10. package/dist/capture/native-environment-tabulated.js.map +1 -1
  11. package/dist/core/portable-math.d.ts +13 -0
  12. package/dist/core/portable-math.js +43 -0
  13. package/dist/core/portable-math.js.map +1 -1
  14. package/dist/core/version.d.ts +1 -1
  15. package/dist/core/version.js +1 -1
  16. package/dist/core/version.js.map +1 -1
  17. package/dist/facade/channel-response.d.ts +15 -0
  18. package/dist/facade/channel-response.js +33 -0
  19. package/dist/facade/channel-response.js.map +1 -0
  20. package/dist/facade/diffraction-provider.js +50 -15
  21. package/dist/facade/diffraction-provider.js.map +1 -1
  22. package/dist/facade/display-view.d.ts +75 -0
  23. package/dist/facade/display-view.js +471 -109
  24. package/dist/facade/display-view.js.map +1 -1
  25. package/dist/facade/expected-signal-setup.js +2 -1
  26. package/dist/facade/expected-signal-setup.js.map +1 -1
  27. package/dist/facade/expected-signal.d.ts +32 -3
  28. package/dist/facade/expected-signal.js +192 -24
  29. package/dist/facade/expected-signal.js.map +1 -1
  30. package/dist/facade/simulate-capture.d.ts +51 -4
  31. package/dist/facade/simulate-capture.js +130 -31
  32. package/dist/facade/simulate-capture.js.map +1 -1
  33. package/dist/facade/simulate-display-view.d.ts +25 -2
  34. package/dist/facade/simulate-display-view.js +55 -22
  35. package/dist/facade/simulate-display-view.js.map +1 -1
  36. package/dist/gpu/image-kernel-execution.d.ts +5 -0
  37. package/dist/gpu/image-kernel-execution.js +2 -2
  38. package/dist/gpu/image-kernel-execution.js.map +1 -1
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +2 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/optics/irradiance-convolution.d.ts +11 -0
  43. package/dist/optics/irradiance-convolution.js +64 -24
  44. package/dist/optics/irradiance-convolution.js.map +1 -1
  45. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  46. package/dist/optics/scene-to-sensor-irradiance.js +58 -24
  47. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  48. package/dist/optics/sensor-aperture-rays.js +2 -1
  49. package/dist/optics/sensor-aperture-rays.js.map +1 -1
  50. package/dist/optics/sensor-environment-query.js +4 -3
  51. package/dist/optics/sensor-environment-query.js.map +1 -1
  52. package/dist/optics/site-diffraction-kernel.d.ts +16 -0
  53. package/dist/optics/site-diffraction-kernel.js +60 -8
  54. package/dist/optics/site-diffraction-kernel.js.map +1 -1
  55. package/dist/processing/demosaic.js +114 -37
  56. package/dist/processing/demosaic.js.map +1 -1
  57. package/dist/schema/scene-radiance.d.ts +8 -0
  58. package/dist/schema/scene-radiance.js +28 -17
  59. package/dist/schema/scene-radiance.js.map +1 -1
  60. package/dist/sensor/environment-photo-signal.d.ts +21 -0
  61. package/dist/sensor/environment-photo-signal.js +68 -2
  62. package/dist/sensor/environment-photo-signal.js.map +1 -1
  63. package/docs/API_REFERENCE.md +16 -1
  64. package/docs/QUICK_START.md +18 -8
  65. package/docs/USAGE.md +30 -0
  66. package/docs/api/core-version.md +1 -1
  67. package/docs/api/exports.json +42 -2
  68. package/docs/api/facade-display-view.md +50 -0
  69. package/docs/api/facade-simulate-capture.md +60 -8
  70. package/docs/api/facade-simulate-display-view.md +23 -1
  71. package/docs/api/gpu-image-kernel-execution.md +5 -0
  72. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"sensor-aperture-rays.js","sourceRoot":"","sources":["../../src/optics/sensor-aperture-rays.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n/**\n * Ideal rays through a symmetric thin-lens pupil. This module does not intersect rays with the\n * scene and does no radiometry.\n */\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { parseEvidenceList, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { calculateFocusPlaneImageDistance, parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nexport interface IdealCircularPupil {\n kind: \"ideal-uniform-circular-pupil\";\n radialSampleCount: number;\n angularSampleCount: number;\n evidence: readonly EvidenceProvenance[];\n limitation: string;\n}\nexport interface SensorApertureRay {\n /** Lens principal-plane origin and look direction in exposure-opening camera axes. */\n originM: { x: number; y: number; z: number };\n directionUnitVector: { x: number; y: number; z: number };\n pupilPointMm: { x: number; y: number };\n weight: number;\n pupilSampleIndex: number;\n}\nexport interface CalculateSensorApertureRaysInput {\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n nominalFNumber: number;\n focus: FocusPlane;\n pupil: IdealCircularPupil;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n timeSecondsFromOpeningReference: number;\n}\nexport interface SensorApertureRays {\n rays: readonly SensorApertureRay[];\n projectionDistanceMm: number;\n pupilRadiusMm: number;\n focus: FocusPlane;\n pupil: IdealCircularPupil;\n sceneVisibilityCalculated: false;\n radiometryCalculated: false;\n apertureAreaApplied: false;\n}\n/**\n * Polar midpoint rule with equal-area cells. Weights are normalized as an average, so they never\n * apply the f-number a second time.\n */\nexport function calculateSensorApertureRays(input: CalculateSensorApertureRaysInput): CalculationResult<SensorApertureRays> {\n requireInputKeys(input, \"calculateSensorApertureRays\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"nominalFNumber\", \"focus\", \"pupil\", \"angularVelocityRadPerSec\", \"timeSecondsFromOpeningReference\"]);\n const owned = structuredClone(input), pupil = owned.pupil;\n requirePositiveFinite(\"nominalFNumber\", owned.nominalFNumber);\n if (!pupil || pupil.kind !== \"ideal-uniform-circular-pupil\" ||\n !Number.isSafeInteger(pupil.radialSampleCount) || pupil.radialSampleCount < 1 || pupil.radialSampleCount > 32 ||\n !Number.isSafeInteger(pupil.angularSampleCount) || pupil.angularSampleCount < 4 || pupil.angularSampleCount > 64 ||\n pupil.radialSampleCount*pupil.angularSampleCount > 256 || typeof pupil.limitation !== \"string\" || !pupil.limitation.trim()) {\n throw new InvalidScientificInputError(\"Circular pupil requires 1..32 radial, 4..64 angular midpoints, at most 256 rays and explicit limitations.\");\n }\n pupil.evidence = parseEvidenceList(pupil.evidence, \"circularPupil.evidence\");\n const focus = parseFocusPlane(owned.focus);\n const { rays, v, radius } = sensorApertureRayValues(owned, pupil, focus);\n return approximationResult({ rays, projectionDistanceMm: v, pupilRadiusMm: radius, focus, pupil,\n sceneVisibilityCalculated: false, radiometryCalculated: false, apertureAreaApplied: false }, \"ideal-thin-lens-circular-pupil-rays\", \"0.1.0\", [\n \"Ideal symmetric paraxial thin lens, unity pupil magnification and uniform circular pupil; no diffraction, aberrations, lens housing or pupil clipping.\",\n \"Normalized pupil averaging is separate from the existing working-f-number radiometric factor; no aperture area or solid-angle factor is applied here.\",\n \"Pure camera rotation about the lens principal-plane origin; no translation or depth-dependent camera motion.\",\n \"Finite polar midpoint quadrature does not establish scene visibility, transport calibration or convergence.\"\n ]);\n}\n\n/**\n * Internal (2.0 performance): the rays of calculateSensorApertureRays, by the same code, for a pupil and focus it has\n * already validated and parsed. The lean native executor calls it per site after one full validation per template.\n * It still runs the rotation, time, image-distance and forward-hemisphere checks, and throws where the full function\n * would. Not exported from the package.\n */\nexport function sensorApertureRayValues(owned: Pick<CalculateSensorApertureRaysInput, \"sourcePointNativeSensorMm\" | \"focalLengthMm\" | \"nominalFNumber\" |\n \"angularVelocityRadPerSec\" | \"timeSecondsFromOpeningReference\">, pupil: Pick<IdealCircularPupil, \"radialSampleCount\" | \"angularSampleCount\">,\n focus: ReturnType<typeof parseFocusPlane>): { rays: SensorApertureRay[]; v: number; radius: number } {\n const v = calculateFocusPlaneImageDistance({ focalLengthMm: owned.focalLengthMm, focus }).value.imageDistanceMm;\n // Reuse the existing rotation and time validation, and the check that rays point forward.\n const inverse = calculateInverseCameraRotationImageMapping({ focalLengthMm: owned.focalLengthMm,\n imagePointMm: { x: owned.sourcePointNativeSensorMm.x, y: -owned.sourcePointNativeSensorMm.y },\n angularVelocityRadPerSec: owned.angularVelocityRadPerSec, timeSecondsFromExposureStart: owned.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) }).value;\n const displacement = inverse.angularDisplacementRad;\n const radius = owned.focalLengthMm/(2*owned.nominalFNumber), count = pupil.radialSampleCount*pupil.angularSampleCount;\n if (!Number.isFinite(radius) || radius <= 0) throw new InvalidScientificInputError(\"Pupil radius must remain positive and finite.\");\n const rays = Array.from({ length: count }, (_, i): SensorApertureRay => {\n const r = radius*Math.sqrt((Math.floor(i/pupil.angularSampleCount)+.5)/pupil.radialSampleCount);\n const theta = 2*Math.PI*((i%pupil.angularSampleCount)+.5)/pupil.angularSampleCount;\n const point = { x: r*Math.cos(theta), y: r*Math.sin(theta) };\n const origin = { x: point.x/1000, y: point.y/1000, z: 0 };\n const x = owned.sourcePointNativeSensorMm.x/v, y = -owned.sourcePointNativeSensorMm.y/v;\n const direction = focus.kind === \"finite\" ? { x: x*focus.distanceM-origin.x, y: y*focus.distanceM-origin.y, z: focus.distanceM }\n : { x, y, z: 1 };\n const length = Math.hypot(direction.x,direction.y,direction.z);\n const unit = { x: direction.x/length, y: direction.y/length, z: direction.z/length };\n // Same physical right-hand camera rotation as the inverse mapping above.\n const rotatedOrigin = applyPhysicalCameraRotation(origin, displacement, \"camera-to-reference\"),\n rotatedDirection = applyPhysicalCameraRotation(unit, displacement, \"camera-to-reference\");\n if (![...Object.values(rotatedOrigin),...Object.values(rotatedDirection)].every(Number.isFinite) || rotatedDirection.z <= 0) {\n throw new InvalidScientificInputError(\"A pupil ray leaves the supported forward hemisphere or finite coordinate domain.\");\n }\n return { originM: rotatedOrigin, directionUnitVector: rotatedDirection, pupilPointMm: point, weight: 1/count, pupilSampleIndex: i };\n });\n return { rays, v, radius };\n}\n"]}
1
+ {"version":3,"file":"sensor-aperture-rays.js","sourceRoot":"","sources":["../../src/optics/sensor-aperture-rays.ts"],"names":[],"mappings":"AAAA,sCAAsC;AACtC;;;GAGG;AACH,OAAO,EAAE,mBAAmB,EAA0B,MAAM,+BAA+B,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAA2B,MAAM,gCAAgC,CAAC;AAC5F,OAAO,EAAE,2BAA2B,EAAE,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AAC3F,OAAO,EAAE,gCAAgC,EAAE,eAAe,EAAmB,MAAM,kBAAkB,CAAC;AACtG,OAAO,EAAE,2BAA2B,EAAE,0CAA0C,EACzC,MAAM,8BAA8B,CAAC;AAC5E,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAChE,OAAO,EAAE,MAAM,EAAE,MAAM,0BAA0B,CAAC;AAmClD;;;GAGG;AACH,MAAM,UAAU,2BAA2B,CAAC,KAAuC;IACjF,gBAAgB,CAAC,KAAK,EAAE,6BAA6B,EAAE,CAAC,2BAA2B,EAAE,eAAe,EAAE,gBAAgB,EAAE,OAAO,EAAE,OAAO,EAAE,0BAA0B,EAAE,iCAAiC,CAAC,CAAC,CAAC;IAC1M,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC;IAC1D,qBAAqB,CAAC,gBAAgB,EAAE,KAAK,CAAC,cAAc,CAAC,CAAC;IAC9D,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,KAAK,8BAA8B;QACzD,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,iBAAiB,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,CAAC,IAAI,KAAK,CAAC,iBAAiB,GAAG,EAAE;QAC7G,CAAC,MAAM,CAAC,aAAa,CAAC,KAAK,CAAC,kBAAkB,CAAC,IAAI,KAAK,CAAC,kBAAkB,GAAG,CAAC,IAAI,KAAK,CAAC,kBAAkB,GAAG,EAAE;QAChH,KAAK,CAAC,iBAAiB,GAAC,KAAK,CAAC,kBAAkB,GAAG,GAAG,IAAI,OAAO,KAAK,CAAC,UAAU,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7H,MAAM,IAAI,2BAA2B,CAAC,2GAA2G,CAAC,CAAC;IACrJ,CAAC;IACD,KAAK,CAAC,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,QAAQ,EAAE,wBAAwB,CAAC,CAAC;IAC7E,MAAM,KAAK,GAAG,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAC3C,MAAM,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,uBAAuB,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACzE,OAAO,mBAAmB,CAAC,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,EAAE,KAAK;QAC7F,yBAAyB,EAAE,KAAK,EAAE,oBAAoB,EAAE,KAAK,EAAE,mBAAmB,EAAE,KAAK,EAAE,EAAE,qCAAqC,EAAE,OAAO,EAAE;QAC7I,wJAAwJ;QACxJ,uJAAuJ;QACvJ,8GAA8G;QAC9G,6GAA6G;KAC9G,CAAC,CAAC;AACL,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CAAC,KACyB,EAAE,KAA2E,EAC5I,KAAyC;IACzC,MAAM,CAAC,GAAG,gCAAgC,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,CAAC,eAAe,CAAC;IAChH,0FAA0F;IAC1F,MAAM,OAAO,GAAG,0CAA0C,CAAC,EAAE,aAAa,EAAE,KAAK,CAAC,aAAa;QAC7F,YAAY,EAAE,EAAE,CAAC,EAAE,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,EAAE;QAC7F,wBAAwB,EAAE,KAAK,CAAC,wBAAwB,EAAE,4BAA4B,EAAE,KAAK,CAAC,+BAA+B;QAC7H,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,KAAK,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC;IACnF,MAAM,YAAY,GAAG,OAAO,CAAC,sBAAsB,CAAC;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,GAAC,CAAC,CAAC,GAAC,KAAK,CAAC,cAAc,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,iBAAiB,GAAC,KAAK,CAAC,kBAAkB,CAAC;IACtH,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,MAAM,IAAI,CAAC;QAAE,MAAM,IAAI,2BAA2B,CAAC,+CAA+C,CAAC,CAAC;IACpI,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAqB,EAAE;QACrE,MAAM,CAAC,GAAG,MAAM,GAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAC,KAAK,CAAC,kBAAkB,CAAC,GAAC,EAAE,CAAC,GAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;QAChG,MAAM,KAAK,GAAG,CAAC,GAAC,IAAI,CAAC,EAAE,GAAC,CAAC,CAAC,CAAC,GAAC,KAAK,CAAC,kBAAkB,CAAC,GAAC,EAAE,CAAC,GAAC,KAAK,CAAC,kBAAkB,CAAC;QACnF,MAAM,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,GAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,EAAE,CAAC,GAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;QAC7D,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,IAAI,EAAE,CAAC,EAAE,KAAK,CAAC,CAAC,GAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QAC1D,MAAM,CAAC,GAAG,KAAK,CAAC,yBAAyB,CAAC,CAAC,GAAC,CAAC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,yBAAyB,CAAC,CAAC,GAAC,CAAC,CAAC;QACxF,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAC,KAAK,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,GAAC,KAAK,CAAC,SAAS,GAAC,MAAM,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC,SAAS,EAAE;YAC9H,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC;QACnB,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC,EAAC,SAAS,CAAC,CAAC,EAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,IAAI,GAAG,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC,EAAE,SAAS,CAAC,CAAC,GAAC,MAAM,EAAE,CAAC;QACrF,yEAAyE;QACzE,MAAM,aAAa,GAAG,2BAA2B,CAAC,MAAM,EAAE,YAAY,EAAE,qBAAqB,CAAC,EAC5F,gBAAgB,GAAG,2BAA2B,CAAC,IAAI,EAAE,YAAY,EAAE,qBAAqB,CAAC,CAAC;QAC5F,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,EAAC,GAAG,MAAM,CAAC,MAAM,CAAC,gBAAgB,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,gBAAgB,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YAC5H,MAAM,IAAI,2BAA2B,CAAC,kFAAkF,CAAC,CAAC;QAC5H,CAAC;QACD,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,YAAY,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,GAAC,KAAK,EAAE,gBAAgB,EAAE,CAAC,EAAE,CAAC;IACtI,CAAC,CAAC,CAAC;IACH,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC;AAC7B,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n/**\n * Ideal rays through a symmetric thin-lens pupil. This module does not intersect rays with the\n * scene and does no radiometry.\n */\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { parseEvidenceList, type EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { calculateFocusPlaneImageDistance, parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\nexport interface IdealCircularPupil {\n kind: \"ideal-uniform-circular-pupil\";\n radialSampleCount: number;\n angularSampleCount: number;\n evidence: readonly EvidenceProvenance[];\n limitation: string;\n}\nexport interface SensorApertureRay {\n /** Lens principal-plane origin and look direction in exposure-opening camera axes. */\n originM: { x: number; y: number; z: number };\n directionUnitVector: { x: number; y: number; z: number };\n pupilPointMm: { x: number; y: number };\n weight: number;\n pupilSampleIndex: number;\n}\nexport interface CalculateSensorApertureRaysInput {\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n nominalFNumber: number;\n focus: FocusPlane;\n pupil: IdealCircularPupil;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n timeSecondsFromOpeningReference: number;\n}\nexport interface SensorApertureRays {\n rays: readonly SensorApertureRay[];\n projectionDistanceMm: number;\n pupilRadiusMm: number;\n focus: FocusPlane;\n pupil: IdealCircularPupil;\n sceneVisibilityCalculated: false;\n radiometryCalculated: false;\n apertureAreaApplied: false;\n}\n/**\n * Polar midpoint rule with equal-area cells. Weights are normalized as an average, so they never\n * apply the f-number a second time.\n */\nexport function calculateSensorApertureRays(input: CalculateSensorApertureRaysInput): CalculationResult<SensorApertureRays> {\n requireInputKeys(input, \"calculateSensorApertureRays\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"nominalFNumber\", \"focus\", \"pupil\", \"angularVelocityRadPerSec\", \"timeSecondsFromOpeningReference\"]);\n const owned = structuredClone(input), pupil = owned.pupil;\n requirePositiveFinite(\"nominalFNumber\", owned.nominalFNumber);\n if (!pupil || pupil.kind !== \"ideal-uniform-circular-pupil\" ||\n !Number.isSafeInteger(pupil.radialSampleCount) || pupil.radialSampleCount < 1 || pupil.radialSampleCount > 32 ||\n !Number.isSafeInteger(pupil.angularSampleCount) || pupil.angularSampleCount < 4 || pupil.angularSampleCount > 64 ||\n pupil.radialSampleCount*pupil.angularSampleCount > 256 || typeof pupil.limitation !== \"string\" || !pupil.limitation.trim()) {\n throw new InvalidScientificInputError(\"Circular pupil requires 1..32 radial, 4..64 angular midpoints, at most 256 rays and explicit limitations.\");\n }\n pupil.evidence = parseEvidenceList(pupil.evidence, \"circularPupil.evidence\");\n const focus = parseFocusPlane(owned.focus);\n const { rays, v, radius } = sensorApertureRayValues(owned, pupil, focus);\n return approximationResult({ rays, projectionDistanceMm: v, pupilRadiusMm: radius, focus, pupil,\n sceneVisibilityCalculated: false, radiometryCalculated: false, apertureAreaApplied: false }, \"ideal-thin-lens-circular-pupil-rays\", \"0.1.0\", [\n \"Ideal symmetric paraxial thin lens, unity pupil magnification and uniform circular pupil; no diffraction, aberrations, lens housing or pupil clipping.\",\n \"Normalized pupil averaging is separate from the existing working-f-number radiometric factor; no aperture area or solid-angle factor is applied here.\",\n \"Pure camera rotation about the lens principal-plane origin; no translation or depth-dependent camera motion.\",\n \"Finite polar midpoint quadrature does not establish scene visibility, transport calibration or convergence.\"\n ]);\n}\n\n/**\n * Internal (2.0 performance): the rays of calculateSensorApertureRays, by the same code, for a pupil and focus it has\n * already validated and parsed. The lean native executor calls it per site after one full validation per template.\n * It still runs the rotation, time, image-distance and forward-hemisphere checks, and throws where the full function\n * would. Not exported from the package.\n */\nexport function sensorApertureRayValues(owned: Pick<CalculateSensorApertureRaysInput, \"sourcePointNativeSensorMm\" | \"focalLengthMm\" | \"nominalFNumber\" |\n \"angularVelocityRadPerSec\" | \"timeSecondsFromOpeningReference\">, pupil: Pick<IdealCircularPupil, \"radialSampleCount\" | \"angularSampleCount\">,\n focus: ReturnType<typeof parseFocusPlane>): { rays: SensorApertureRay[]; v: number; radius: number } {\n const v = calculateFocusPlaneImageDistance({ focalLengthMm: owned.focalLengthMm, focus }).value.imageDistanceMm;\n // Reuse the existing rotation and time validation, and the check that rays point forward.\n const inverse = calculateInverseCameraRotationImageMapping({ focalLengthMm: owned.focalLengthMm,\n imagePointMm: { x: owned.sourcePointNativeSensorMm.x, y: -owned.sourcePointNativeSensorMm.y },\n angularVelocityRadPerSec: owned.angularVelocityRadPerSec, timeSecondsFromExposureStart: owned.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) }).value;\n const displacement = inverse.angularDisplacementRad;\n const radius = owned.focalLengthMm/(2*owned.nominalFNumber), count = pupil.radialSampleCount*pupil.angularSampleCount;\n if (!Number.isFinite(radius) || radius <= 0) throw new InvalidScientificInputError(\"Pupil radius must remain positive and finite.\");\n const rays = Array.from({ length: count }, (_, i): SensorApertureRay => {\n const r = radius*Math.sqrt((Math.floor(i/pupil.angularSampleCount)+.5)/pupil.radialSampleCount);\n const theta = 2*Math.PI*((i%pupil.angularSampleCount)+.5)/pupil.angularSampleCount;\n const point = { x: r*Math.cos(theta), y: r*Math.sin(theta) };\n const origin = { x: point.x/1000, y: point.y/1000, z: 0 };\n const x = owned.sourcePointNativeSensorMm.x/v, y = -owned.sourcePointNativeSensorMm.y/v;\n const direction = focus.kind === \"finite\" ? { x: x*focus.distanceM-origin.x, y: y*focus.distanceM-origin.y, z: focus.distanceM }\n : { x, y, z: 1 };\n const length = hypot3(direction.x,direction.y,direction.z);\n const unit = { x: direction.x/length, y: direction.y/length, z: direction.z/length };\n // Same physical right-hand camera rotation as the inverse mapping above.\n const rotatedOrigin = applyPhysicalCameraRotation(origin, displacement, \"camera-to-reference\"),\n rotatedDirection = applyPhysicalCameraRotation(unit, displacement, \"camera-to-reference\");\n if (![...Object.values(rotatedOrigin),...Object.values(rotatedDirection)].every(Number.isFinite) || rotatedDirection.z <= 0) {\n throw new InvalidScientificInputError(\"A pupil ray leaves the supported forward hemisphere or finite coordinate domain.\");\n }\n return { originM: rotatedOrigin, directionUnitVector: rotatedDirection, pupilPointMm: point, weight: 1/count, pupilSampleIndex: i };\n });\n return { rays, v, radius };\n}\n"]}
@@ -29,6 +29,7 @@ import { parseSceneRadianceEvaluationRequest } from "../schema/scene-radiance.js
29
29
  import { parseFocusPlane } from "./focus-state.js";
30
30
  import { requireInputKeys } from "../core/record-validation.js";
31
31
  import { InvalidConfigurationError } from "../core/configuration-error.js";
32
+ import { hypot3 } from "../core/portable-math.js";
32
33
  /** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */
33
34
  export function prepareEnvironmentLensFieldMapping(value) {
34
35
  if (typeof value !== "object" || value === null || Array.isArray(value)) {
@@ -88,7 +89,7 @@ export function calculateSensorEnvironmentRadianceQuery(input) {
88
89
  timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,
89
90
  ...(focus.kind === "finite" ? { focusDistanceM: focus.distanceM } : {}) });
90
91
  const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;
91
- const length = Math.hypot(point.x, point.y, z);
92
+ const length = hypot3(point.x, point.y, z);
92
93
  const referenceLookDirectionUnitVector = { x: point.x / length, y: point.y / length, z: z / length };
93
94
  const ray = referenceLookDirectionUnitVector;
94
95
  const request = parseSceneRadianceEvaluationRequest({ ...input.request,
@@ -144,7 +145,7 @@ export function calculateMappedSensorEnvironmentRadianceQuery(input, prepared) {
144
145
  timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,
145
146
  ...(focus.kind === "finite" ? { focusDistanceM: focus.distanceM } : {}) });
146
147
  const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;
147
- const length = Math.hypot(point.x, point.y, z);
148
+ const length = hypot3(point.x, point.y, z);
148
149
  look = { x: point.x / length, y: point.y / length, z: z / length };
149
150
  projectionScaleMm = z;
150
151
  }
@@ -159,7 +160,7 @@ export function calculateMappedSensorEnvironmentRadianceQuery(input, prepared) {
159
160
  }
160
161
  const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);
161
162
  const reference = applyPhysicalCameraRotation(captured, { pitch: velocity.pitch * t, yaw: velocity.yaw * t, roll: velocity.roll * t }, "camera-to-reference");
162
- const length = Math.hypot(reference.x, reference.y, reference.z);
163
+ const length = hypot3(reference.x, reference.y, reference.z);
163
164
  if (!(length > 0) || !Number.isFinite(length))
164
165
  throw new InvalidScientificInputError("Mapped reference ray must be finite.");
165
166
  look = { x: reference.x / length, y: reference.y / length, z: reference.z / length };
@@ -1 +1 @@
1
- {"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = Math.hypot(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n let ideal = sourcePointImagePlaneMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(sourcePointImagePlaneMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, sourcePointImagePlaneMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = Math.hypot(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = Math.hypot(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
1
+ {"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n let ideal = sourcePointImagePlaneMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(sourcePointImagePlaneMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, sourcePointImagePlaneMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = hypot3(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
@@ -1,5 +1,21 @@
1
1
  import { resolveLensPsfConvolutionKernels } from "./lens-psf-kernels.js";
2
2
  type Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0]["psf"];
3
+ /** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */
4
+ export declare const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;
5
+ /**
6
+ * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the
7
+ * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the
8
+ * record; neither the record nor `profileInput` may change afterwards.
9
+ */
10
+ export declare function registerDiffractionPsfKey(psf: Psf, profileInput: object): void;
3
11
  /** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */
4
12
  export declare function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array;
13
+ /** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */
14
+ export declare function siteDiffractionKernelCacheState(psf?: Psf): {
15
+ valueEntries: number;
16
+ resolves: number;
17
+ registered: boolean;
18
+ };
19
+ /** Test hook: forgets every kernel kept by value. */
20
+ export declare function clearSiteDiffractionKernelCache(): void;
5
21
  export {};
@@ -4,19 +4,62 @@
4
4
  * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.
5
5
  *
6
6
  * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users
7
- * ask for the same kernels, so they are kept while the PSF record lives. The kernel is a function of its inputs alone; each caller
8
- * gets its own copy.
7
+ * ask for the same kernels. The kernel is a function of its inputs alone; each caller gets its own copy.
8
+ *
9
+ * The facade builds a new PSF record for every request, so kernels are kept by value: `registerDiffractionPsfKey` records, for a
10
+ * record whose profile `createDiffractionLimitedPsfProfile` built, a canonical string of that call's whole input and of the record's
11
+ * own fields (everything the kernel is a function of besides the wavelength, pitch and radius of the query). Records with an equal
12
+ * string share their kernels, in a least-recently-used cache of `SITE_DIFFRACTION_KERNEL_CACHE_SIZE` records. A record that was not
13
+ * registered (a caller's own profile) keeps its kernels while it lives, by identity, as before.
9
14
  */
10
15
  import { InvalidConfigurationError } from "../core/configuration-error.js";
11
16
  import { resolveLensPsfConvolutionKernels } from "./lens-psf-kernels.js";
12
- /** Kernels per PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */
13
- const cache = new WeakMap();
17
+ /** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */
18
+ export const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;
19
+ /** Canonical value key of each registered PSF record. */
20
+ const keys = new WeakMap();
21
+ /** Kernels per registered value key (least recently used first), then per wavelength, pitch and radius. */
22
+ const byValue = new Map();
23
+ /** Kernels per unregistered PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */
24
+ const byIdentity = new WeakMap();
25
+ /** Kernels resolved so far (test hook). */
26
+ let resolves = 0;
27
+ /** JSON with every number spelled exactly: shortest round-trip digits, and -0, NaN and ±Infinity kept apart from 0 and null. */
28
+ function canonical(value) {
29
+ return JSON.stringify(value, (_key, v) => (typeof v === "number" && (Object.is(v, -0) || !Number.isFinite(v)) ? "number:" + (Object.is(v, -0) ? "-0" : String(v)) : v));
30
+ }
31
+ /**
32
+ * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the
33
+ * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the
34
+ * record; neither the record nor `profileInput` may change afterwards.
35
+ */
36
+ export function registerDiffractionPsfKey(psf, profileInput) {
37
+ const fields = Object.entries(psf).filter(([name]) => name !== "profile").sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
38
+ keys.set(psf, canonical(["createDiffractionLimitedPsfProfile", profileInput, fields]));
39
+ }
40
+ /** The kernels kept for `psf`, made the most recently used. */
41
+ function kernelsOf(psf) {
42
+ const key = keys.get(psf);
43
+ if (key === undefined) {
44
+ let kernels = byIdentity.get(psf);
45
+ if (!kernels)
46
+ byIdentity.set(psf, (kernels = new Map()));
47
+ return kernels;
48
+ }
49
+ let kernels = byValue.get(key);
50
+ if (kernels)
51
+ byValue.delete(key);
52
+ else
53
+ kernels = new Map();
54
+ byValue.set(key, kernels);
55
+ while (byValue.size > SITE_DIFFRACTION_KERNEL_CACHE_SIZE)
56
+ byValue.delete(byValue.keys().next().value);
57
+ return kernels;
58
+ }
14
59
  /** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */
15
60
  export function siteDiffractionKernel(psf, wavelengthNm, pitchMicrometers, radius, failure) {
16
- let kernels = cache.get(psf);
17
- if (!kernels)
18
- cache.set(psf, (kernels = new Map()));
19
- const key = wavelengthNm + ":" + pitchMicrometers + ":" + radius;
61
+ const kernels = kernelsOf(psf);
62
+ const key = canonical([wavelengthNm, pitchMicrometers, radius]);
20
63
  let kernel = kernels.get(key);
21
64
  if (!kernel) {
22
65
  const k = resolveLensPsfConvolutionKernels({ schemaVersion: "0.1.0", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],
@@ -25,8 +68,17 @@ export function siteDiffractionKernel(psf, wavelengthNm, pitchMicrometers, radiu
25
68
  if (!k.exactSiteSampling || k.kernelRadius !== radius)
26
69
  throw new InvalidConfigurationError(failure);
27
70
  kernel = Float64Array.from(k.kernels);
71
+ resolves++;
28
72
  kernels.set(key, kernel);
29
73
  }
30
74
  return Float64Array.from(kernel);
31
75
  }
76
+ /** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */
77
+ export function siteDiffractionKernelCacheState(psf) {
78
+ return { valueEntries: byValue.size, resolves, registered: psf !== undefined && keys.has(psf) };
79
+ }
80
+ /** Test hook: forgets every kernel kept by value. */
81
+ export function clearSiteDiffractionKernelCache() {
82
+ byValue.clear();
83
+ }
32
84
  //# sourceMappingURL=site-diffraction-kernel.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"site-diffraction-kernel.js","sourceRoot":"","sources":["../../src/optics/site-diffraction-kernel.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;GAOG;AAEH,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAC3E,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AAIzE,iIAAiI;AACjI,MAAM,KAAK,GAAG,IAAI,OAAO,EAAqC,CAAC;AAE/D,yLAAyL;AACzL,MAAM,UAAU,qBAAqB,CAAC,GAAQ,EAAE,YAAoB,EAAE,gBAAwB,EAAE,MAAc,EAAE,OAAe;IAC7H,IAAI,OAAO,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC7B,IAAI,CAAC,OAAO;QAAE,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,OAAO,GAAG,IAAI,GAAG,EAAE,CAAC,CAAC,CAAC;IACpD,MAAM,GAAG,GAAG,YAAY,GAAG,GAAG,GAAG,gBAAgB,GAAG,GAAG,GAAG,MAAM,CAAC;IACjE,IAAI,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;IAC9B,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,MAAM,CAAC,GAAG,gCAAgC,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,EAAE,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;YACtH,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,oBAAoB,EAAE,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,gBAAgB,EAAE,EAAE,eAAe,EAAE,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,GAAG,EAAE,EAAE,EAAE,UAAU,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC;YACrK,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,KAAK,CAAC;QAChC,IAAI,CAAC,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC,YAAY,KAAK,MAAM;YAAE,MAAM,IAAI,yBAAyB,CAAC,OAAO,CAAC,CAAC;QACpG,MAAM,GAAG,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;QACtC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;IAC3B,CAAC;IACD,OAAO,YAAY,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AACnC,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): one wavelength's diffraction kernel sampled on the site pitch, as the object-space\n * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.\n *\n * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users\n * ask for the same kernels, so they are kept while the PSF record lives. The kernel is a function of its inputs alone; each caller\n * gets its own copy.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { resolveLensPsfConvolutionKernels } from \"./lens-psf-kernels.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\n/** Kernels per PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */\nconst cache = new WeakMap<object, Map<string, Float64Array>>();\n\n/** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */\nexport function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array {\n let kernels = cache.get(psf);\n if (!kernels) cache.set(psf, (kernels = new Map()));\n const key = wavelengthNm + \":\" + pitchMicrometers + \":\" + radius;\n let kernel = kernels.get(key);\n if (!kernel) {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitchMicrometers, y: pitchMicrometers }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) throw new InvalidConfigurationError(failure);\n kernel = Float64Array.from(k.kernels);\n kernels.set(key, kernel);\n }\n return Float64Array.from(kernel);\n}\n"]}
1
+ {"version":3,"file":"site-diffraction-kernel.js","sourceRoot":"","sources":["../../src/optics/site-diffraction-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Internal (not exported from the package): one wavelength's diffraction kernel sampled on the site pitch, as the object-space\n * convolution (facade/diffraction-provider.ts) and the tabulated expected signal (facade/expected-signal-setup.ts) use it.\n *\n * `resolveLensPsfConvolutionKernels` parses and samples the PSF profile on every call (about 10 ms per wavelength), and both users\n * ask for the same kernels. The kernel is a function of its inputs alone; each caller gets its own copy.\n *\n * The facade builds a new PSF record for every request, so kernels are kept by value: `registerDiffractionPsfKey` records, for a\n * record whose profile `createDiffractionLimitedPsfProfile` built, a canonical string of that call's whole input and of the record's\n * own fields (everything the kernel is a function of besides the wavelength, pitch and radius of the query). Records with an equal\n * string share their kernels, in a least-recently-used cache of `SITE_DIFFRACTION_KERNEL_CACHE_SIZE` records. A record that was not\n * registered (a caller's own profile) keeps its kernels while it lives, by identity, as before.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { resolveLensPsfConvolutionKernels } from \"./lens-psf-kernels.js\";\n\ntype Psf = Parameters<typeof resolveLensPsfConvolutionKernels>[0][\"psf\"];\n\n/** PSF records kept by value: at most 20 kernels of (2·32 + 1)² doubles each (676 KB) per record at the default spectral sampling. */\nexport const SITE_DIFFRACTION_KERNEL_CACHE_SIZE = 4;\n\n/** Canonical value key of each registered PSF record. */\nconst keys = new WeakMap<object, string>();\n/** Kernels per registered value key (least recently used first), then per wavelength, pitch and radius. */\nconst byValue = new Map<string, Map<string, Float64Array>>();\n/** Kernels per unregistered PSF record (by identity: the record is large, so it is not serialized), then per wavelength, pitch and radius. */\nconst byIdentity = new WeakMap<object, Map<string, Float64Array>>();\n/** Kernels resolved so far (test hook). */\nlet resolves = 0;\n\n/** JSON with every number spelled exactly: shortest round-trip digits, and -0, NaN and ±Infinity kept apart from 0 and null. */\nfunction canonical(value: unknown): string {\n return JSON.stringify(value, (_key, v: unknown) => (typeof v === \"number\" && (Object.is(v, -0) || !Number.isFinite(v)) ? \"number:\" + (Object.is(v, -0) ? \"-0\" : String(v)) : v));\n}\n\n/**\n * Records that `psf.profile` is the profile `createDiffractionLimitedPsfProfile(profileInput)` returned, unmodified, so that the\n * kernels of `psf` are those of any record with the same `profileInput` and the same other fields. Call it right after building the\n * record; neither the record nor `profileInput` may change afterwards.\n */\nexport function registerDiffractionPsfKey(psf: Psf, profileInput: object): void {\n const fields = Object.entries(psf as object).filter(([name]) => name !== \"profile\").sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));\n keys.set(psf as object, canonical([\"createDiffractionLimitedPsfProfile\", profileInput, fields]));\n}\n\n/** The kernels kept for `psf`, made the most recently used. */\nfunction kernelsOf(psf: Psf): Map<string, Float64Array> {\n const key = keys.get(psf as object);\n if (key === undefined) {\n let kernels = byIdentity.get(psf as object);\n if (!kernels) byIdentity.set(psf as object, (kernels = new Map()));\n return kernels;\n }\n let kernels = byValue.get(key);\n if (kernels) byValue.delete(key);\n else kernels = new Map();\n byValue.set(key, kernels);\n while (byValue.size > SITE_DIFFRACTION_KERNEL_CACHE_SIZE) byValue.delete(byValue.keys().next().value!);\n return kernels;\n}\n\n/** The (2·radius + 1)² kernel of `psf` at `wavelengthNm` on a square site grid of `pitchMicrometers`; `failure` is the error message when the kernel cannot sample the pitch exactly. */\nexport function siteDiffractionKernel(psf: Psf, wavelengthNm: number, pitchMicrometers: number, radius: number, failure: string): Float64Array {\n const kernels = kernelsOf(psf);\n const key = canonical([wavelengthNm, pitchMicrometers, radius]);\n let kernel = kernels.get(key);\n if (!kernel) {\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitchMicrometers, y: pitchMicrometers }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1,\n kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) throw new InvalidConfigurationError(failure);\n kernel = Float64Array.from(k.kernels);\n resolves++;\n kernels.set(key, kernel);\n }\n return Float64Array.from(kernel);\n}\n\n/** Test hook: the number of PSF records kept by value, the kernels resolved so far, and whether `psf` is registered. */\nexport function siteDiffractionKernelCacheState(psf?: Psf): { valueEntries: number; resolves: number; registered: boolean } {\n return { valueEntries: byValue.size, resolves, registered: psf !== undefined && keys.has(psf as object) };\n}\n\n/** Test hook: forgets every kernel kept by value. */\nexport function clearSiteDiffractionKernelCache(): void {\n byValue.clear();\n}\n"]}
@@ -165,9 +165,14 @@ export function createAhdDemosaicTask(input, provider) {
165
165
  const W = p.width, H = p.height, s = p.samples;
166
166
  const roleAt = (x, y) => tile[(y & 1) * 2 + (x & 1)];
167
167
  const at = (x, y) => s[reflect(y, H) * W + reflect(x, W)];
168
+ // Sites whose neighbourhood lies inside the plane (two sites from the edge for stage 1, one for stages 2, 4
169
+ // and 5) read their neighbours by direct index offsets; reflect() returns its argument there, so they address
170
+ // the same samples as the border path. Every floating-point expression keeps the operands, order and grouping
171
+ // of the border path, so both paths give bit-identical results (test/processing-ahd-exactness.test.ts).
168
172
  // 1. Directional greens.
169
173
  const gh = new Float32Array(n), gv = new Float32Array(n);
170
174
  for (let y = 0; y < H; y++) {
175
+ const interiorRow = y >= 2 && y + 2 < H;
171
176
  for (let x = 0; x < W; x++) {
172
177
  const i = y * W + x, c = s[i];
173
178
  if (roleAt(x, y) === 1) {
@@ -175,28 +180,54 @@ export function createAhdDemosaicTask(input, provider) {
175
180
  gv[i] = c;
176
181
  continue;
177
182
  }
178
- gh[i] = (at(x - 1, y) + at(x + 1, y)) / 2 + (2 * c - at(x - 2, y) - at(x + 2, y)) / 4;
179
- gv[i] = (at(x, y - 1) + at(x, y + 1)) / 2 + (2 * c - at(x, y - 2) - at(x, y + 2)) / 4;
183
+ if (interiorRow && x >= 2 && x + 2 < W) {
184
+ gh[i] = (s[i - 1] + s[i + 1]) / 2 + (2 * c - s[i - 2] - s[i + 2]) / 4;
185
+ gv[i] = (s[i - W] + s[i + W]) / 2 + (2 * c - s[i - 2 * W] - s[i + 2 * W]) / 4;
186
+ }
187
+ else {
188
+ gh[i] = (at(x - 1, y) + at(x + 1, y)) / 2 + (2 * c - at(x - 2, y) - at(x + 2, y)) / 4;
189
+ gv[i] = (at(x, y - 1) + at(x, y + 1)) / 2 + (2 * c - at(x, y - 2) - at(x, y + 2)) / 4;
190
+ }
180
191
  }
181
192
  yield;
182
193
  }
183
194
  // 2. Two full-colour candidates from colour differences.
184
195
  const rgb = [new Float32Array(3 * n), new Float32Array(3 * n)], greens = [gh, gv];
185
196
  for (let y = 0; y < H; y++) {
197
+ const interiorRow = y >= 1 && y + 1 < H;
186
198
  for (let x = 0; x < W; x++) {
187
- const i = y * W + x, own = roleAt(x, y);
199
+ const i = y * W + x, own = roleAt(x, y), interior = interiorRow && x >= 1 && x + 1 < W;
188
200
  for (let d = 0; d < 2; d++) {
189
- const g = greens[d], out = rgb[d];
190
- const diff = (xx, yy) => { const j = reflect(yy, H) * W + reflect(xx, W); return s[j] - g[j]; };
191
- out[3 * i + 1] = g[i];
201
+ const g = greens[d], out = rgb[d], gi = g[i];
202
+ out[3 * i + 1] = gi;
192
203
  if (own === 1) {
193
204
  const hRole = roleAt(x + 1, y), vRole = 2 - hRole;
194
- out[3 * i + hRole] = g[i] + (diff(x - 1, y) + diff(x + 1, y)) / 2;
195
- out[3 * i + vRole] = g[i] + (diff(x, y - 1) + diff(x, y + 1)) / 2;
205
+ if (interior) {
206
+ out[3 * i + hRole] = gi + ((s[i - 1] - g[i - 1]) + (s[i + 1] - g[i + 1])) / 2;
207
+ out[3 * i + vRole] = gi + ((s[i - W] - g[i - W]) + (s[i + W] - g[i + W])) / 2;
208
+ }
209
+ else {
210
+ let j = reflect(y, H) * W + reflect(x - 1, W);
211
+ const a = s[j] - g[j];
212
+ j = reflect(y, H) * W + reflect(x + 1, W);
213
+ const b = s[j] - g[j];
214
+ out[3 * i + hRole] = gi + (a + b) / 2;
215
+ j = reflect(y - 1, H) * W + reflect(x, W);
216
+ const cc = s[j] - g[j];
217
+ j = reflect(y + 1, H) * W + reflect(x, W);
218
+ const dd = s[j] - g[j];
219
+ out[3 * i + vRole] = gi + (cc + dd) / 2;
220
+ }
196
221
  }
197
222
  else {
198
223
  out[3 * i + own] = s[i];
199
- out[3 * i + 2 - own] = g[i] + (diff(x - 1, y - 1) + diff(x + 1, y - 1) + diff(x - 1, y + 1) + diff(x + 1, y + 1)) / 4;
224
+ if (interior) {
225
+ out[3 * i + 2 - own] = gi + ((s[i - W - 1] - g[i - W - 1]) + (s[i - W + 1] - g[i - W + 1]) + (s[i + W - 1] - g[i + W - 1]) + (s[i + W + 1] - g[i + W + 1])) / 4;
226
+ }
227
+ else {
228
+ const ym = reflect(y - 1, H) * W, yp = reflect(y + 1, H) * W, xm = reflect(x - 1, W), xp = reflect(x + 1, W);
229
+ out[3 * i + 2 - own] = gi + ((s[ym + xm] - g[ym + xm]) + (s[ym + xp] - g[ym + xp]) + (s[yp + xm] - g[yp + xm]) + (s[yp + xp] - g[yp + xp])) / 4;
230
+ }
200
231
  }
201
232
  }
202
233
  }
@@ -205,12 +236,14 @@ export function createAhdDemosaicTask(input, provider) {
205
236
  // 3. CIELAB (W3C CSS Color 4 Lab definition; also CIE 15) of both candidates.
206
237
  const lab = [new Float32Array(3 * n), new Float32Array(3 * n)];
207
238
  const f = (t) => t > 216 / 24389 ? Math.cbrt(t) : (24389 / 27 * t + 16) / 116;
239
+ const m00 = m[0][0], m01 = m[0][1], m02 = m[0][2], m10 = m[1][0], m11 = m[1][1], m12 = m[1][2], m20 = m[2][0], m21 = m[2][1], m22 = m[2][2];
240
+ const w0 = white[0], w1 = white[1], w2 = white[2];
208
241
  for (let y = 0; y < H; y++) {
209
242
  for (let i = y * W; i < (y + 1) * W; i++)
210
243
  for (let d = 0; d < 2; d++) {
211
244
  const c = rgb[d], R = c[3 * i], G = c[3 * i + 1], B = c[3 * i + 2];
212
- const fx = f((m[0][0] * R + m[0][1] * G + m[0][2] * B) / white[0]), fy = f((m[1][0] * R + m[1][1] * G + m[1][2] * B) / white[1]);
213
- const fz = f((m[2][0] * R + m[2][1] * G + m[2][2] * B) / white[2]), o = lab[d];
245
+ const fx = f((m00 * R + m01 * G + m02 * B) / w0), fy = f((m10 * R + m11 * G + m12 * B) / w1);
246
+ const fz = f((m20 * R + m21 * G + m22 * B) / w2), o = lab[d];
214
247
  o[3 * i] = 116 * fy - 16;
215
248
  o[3 * i + 1] = 500 * (fx - fy);
216
249
  o[3 * i + 2] = 200 * (fy - fz);
@@ -218,46 +251,90 @@ export function createAhdDemosaicTask(input, provider) {
218
251
  yield;
219
252
  }
220
253
  // 4. Homogeneity maps with adaptive tolerances.
221
- const homo = [new Uint8Array(n), new Uint8Array(n)];
222
- const N4 = [[-1, 0], [1, 0], [0, -1], [0, 1]];
254
+ const homo = [new Uint8Array(n), new Uint8Array(n)], lab0 = lab[0], lab1 = lab[1];
223
255
  for (let y = 0; y < H; y++) {
256
+ const interiorRow = y >= 1 && y + 1 < H;
224
257
  for (let x = 0; x < W; x++) {
225
- const i = y * W + x, dL = [[0, 0, 0, 0], [0, 0, 0, 0]], dC = [[0, 0, 0, 0], [0, 0, 0, 0]];
226
- for (let d = 0; d < 2; d++) {
227
- const o = lab[d];
228
- for (let k = 0; k < 4; k++) {
229
- const j = reflect(y + N4[k][1], H) * W + reflect(x + N4[k][0], W);
230
- dL[d][k] = Math.abs(o[3 * i] - o[3 * j]);
231
- dC[d][k] = (o[3 * i + 1] - o[3 * j + 1]) ** 2 + (o[3 * i + 2] - o[3 * j + 2]) ** 2;
232
- }
258
+ const i = y * W + x;
259
+ let jl, jr, ju, jd;
260
+ if (interiorRow && x >= 1 && x + 1 < W) {
261
+ jl = i - 1;
262
+ jr = i + 1;
263
+ ju = i - W;
264
+ jd = i + W;
233
265
  }
234
- const epsL = Math.min(Math.max(dL[0][0], dL[0][1]), Math.max(dL[1][2], dL[1][3]));
235
- const epsC = Math.min(Math.max(dC[0][0], dC[0][1]), Math.max(dC[1][2], dC[1][3]));
236
- for (let d = 0; d < 2; d++) {
237
- let count = 0;
238
- for (let k = 0; k < 4; k++)
239
- if (dL[d][k] <= epsL && dC[d][k] <= epsC)
240
- count++;
241
- homo[d][i] = count;
266
+ else {
267
+ const row = reflect(y, H) * W, col = reflect(x, W);
268
+ jl = row + reflect(x - 1, W);
269
+ jr = row + reflect(x + 1, W);
270
+ ju = reflect(y - 1, H) * W + col;
271
+ jd = reflect(y + 1, H) * W + col;
242
272
  }
273
+ // Direction 0 (lab0), neighbours left, right, up, down (the N4 order).
274
+ const L0 = lab0[3 * i], a0 = lab0[3 * i + 1], b0 = lab0[3 * i + 2];
275
+ const dL00 = Math.abs(L0 - lab0[3 * jl]), dC00 = (a0 - lab0[3 * jl + 1]) ** 2 + (b0 - lab0[3 * jl + 2]) ** 2;
276
+ const dL01 = Math.abs(L0 - lab0[3 * jr]), dC01 = (a0 - lab0[3 * jr + 1]) ** 2 + (b0 - lab0[3 * jr + 2]) ** 2;
277
+ const dL02 = Math.abs(L0 - lab0[3 * ju]), dC02 = (a0 - lab0[3 * ju + 1]) ** 2 + (b0 - lab0[3 * ju + 2]) ** 2;
278
+ const dL03 = Math.abs(L0 - lab0[3 * jd]), dC03 = (a0 - lab0[3 * jd + 1]) ** 2 + (b0 - lab0[3 * jd + 2]) ** 2;
279
+ const L1 = lab1[3 * i], a1 = lab1[3 * i + 1], b1 = lab1[3 * i + 2];
280
+ const dL10 = Math.abs(L1 - lab1[3 * jl]), dC10 = (a1 - lab1[3 * jl + 1]) ** 2 + (b1 - lab1[3 * jl + 2]) ** 2;
281
+ const dL11 = Math.abs(L1 - lab1[3 * jr]), dC11 = (a1 - lab1[3 * jr + 1]) ** 2 + (b1 - lab1[3 * jr + 2]) ** 2;
282
+ const dL12 = Math.abs(L1 - lab1[3 * ju]), dC12 = (a1 - lab1[3 * ju + 1]) ** 2 + (b1 - lab1[3 * ju + 2]) ** 2;
283
+ const dL13 = Math.abs(L1 - lab1[3 * jd]), dC13 = (a1 - lab1[3 * jd + 1]) ** 2 + (b1 - lab1[3 * jd + 2]) ** 2;
284
+ const epsL = Math.min(Math.max(dL00, dL01), Math.max(dL12, dL13));
285
+ const epsC = Math.min(Math.max(dC00, dC01), Math.max(dC12, dC13));
286
+ let count = 0;
287
+ if (dL00 <= epsL && dC00 <= epsC)
288
+ count++;
289
+ if (dL01 <= epsL && dC01 <= epsC)
290
+ count++;
291
+ if (dL02 <= epsL && dC02 <= epsC)
292
+ count++;
293
+ if (dL03 <= epsL && dC03 <= epsC)
294
+ count++;
295
+ homo[0][i] = count;
296
+ count = 0;
297
+ if (dL10 <= epsL && dC10 <= epsC)
298
+ count++;
299
+ if (dL11 <= epsL && dC11 <= epsC)
300
+ count++;
301
+ if (dL12 <= epsL && dC12 <= epsC)
302
+ count++;
303
+ if (dL13 <= epsL && dC13 <= epsC)
304
+ count++;
305
+ homo[1][i] = count;
243
306
  }
244
307
  yield;
245
308
  }
246
309
  // 5. 3×3 summed homogeneity selects the candidate.
247
- const out = new Float32Array(3 * n), direction = new Uint8Array(n);
310
+ const out = new Float32Array(3 * n), direction = new Uint8Array(n), h0 = homo[0], h1 = homo[1], rgb0 = rgb[0], rgb1 = rgb[1];
248
311
  for (let y = 0; y < H; y++) {
312
+ const interiorRow = y >= 1 && y + 1 < H;
249
313
  for (let x = 0; x < W; x++) {
250
314
  let sh = 0, sv = 0;
251
- for (let dy = -1; dy <= 1; dy++)
252
- for (let dx = -1; dx <= 1; dx++) {
253
- const j = reflect(y + dy, H) * W + reflect(x + dx, W);
254
- sh += homo[0][j];
255
- sv += homo[1][j];
315
+ const i = y * W + x;
316
+ if (interiorRow && x >= 1 && x + 1 < W) {
317
+ for (let dy = -1; dy <= 1; dy++) {
318
+ const row = i + dy * W;
319
+ for (let dx = -1; dx <= 1; dx++) {
320
+ const j = row + dx;
321
+ sh += h0[j];
322
+ sv += h1[j];
323
+ }
256
324
  }
257
- const i = y * W + x, dir = sh > sv ? 0 : sv > sh ? 1 : 2;
325
+ }
326
+ else {
327
+ for (let dy = -1; dy <= 1; dy++)
328
+ for (let dx = -1; dx <= 1; dx++) {
329
+ const j = reflect(y + dy, H) * W + reflect(x + dx, W);
330
+ sh += h0[j];
331
+ sv += h1[j];
332
+ }
333
+ }
334
+ const dir = sh > sv ? 0 : sv > sh ? 1 : 2;
258
335
  direction[i] = dir;
259
336
  for (let c = 0; c < 3; c++) {
260
- const a = rgb[0][3 * i + c], b = rgb[1][3 * i + c];
337
+ const a = rgb0[3 * i + c], b = rgb1[3 * i + c];
261
338
  out[3 * i + c] = dir === 0 ? a : dir === 1 ? b : (a + b) / 2;
262
339
  }
263
340
  }