@photivra/engine 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -0
- package/dist/capture/native-environment-lean.js +1 -1
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-raw-development.d.ts +15 -0
- package/dist/capture/native-raw-development.js +12 -6
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/capture/raw-development-kernel.d.ts +10 -3
- package/dist/capture/raw-development-kernel.js +25 -6
- package/dist/capture/raw-development-kernel.js.map +1 -1
- package/dist/core/internal-radiance-evaluator.d.ts +5 -2
- package/dist/core/internal-radiance-evaluator.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +109 -0
- package/dist/facade/defocus-image-space-provider.js +359 -0
- package/dist/facade/defocus-image-space-provider.js.map +1 -0
- package/dist/facade/diffraction-provider.d.ts +16 -0
- package/dist/facade/diffraction-provider.js +3 -3
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +21 -0
- package/dist/facade/display-view.js +66 -8
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +111 -2
- package/dist/facade/simulate-capture.js +220 -44
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +7 -3
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +2 -0
- package/dist/gpu/display-view-quality-gpu.js +57 -13
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +2 -0
- package/dist/gpu/display-view-speed-gpu.js +45 -16
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-accelerator.js +3 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +53 -2
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +15 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +42 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +3 -2
- package/dist/optics/defocus-disc-kernel.js +3 -2
- package/dist/optics/defocus-disc-kernel.js.map +1 -1
- package/dist/optics/sensor-environment-query.d.ts +31 -0
- package/dist/optics/sensor-environment-query.js +45 -13
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/processing/development-detail.d.ts +132 -0
- package/dist/processing/development-detail.js +325 -0
- package/dist/processing/development-detail.js.map +1 -0
- package/docs/API_REFERENCE.md +6 -1
- package/docs/QUICK_START.md +190 -12
- package/docs/USAGE.md +14 -3
- package/docs/api/capture-native-raw-development.md +16 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +27 -2
- package/docs/api/facade-simulate-capture.md +128 -3
- package/docs/api/optics-defocus-disc-kernel.md +3 -2
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"defocus-disc-kernel.js","sourceRoot":"","sources":["../../src/optics/defocus-disc-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The geometric defocus disc of a flat scene, sampled on the square site lattice, for\n * `lens.defocus: { model: \"geometric-disc\" }` (#492). Internal except for the radius cap\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} and the pre-check {@link calculateDefocusDiscKernelRadius}, which are\n * root exports (#532), so a caller can check a request against the cap before rendering.\n *\n * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil\n * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor\n * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused\n * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent\n * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).\n *\n * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at\n * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided\n * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of\n * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with\n * x, y clamped to the radius r\n * Q = x·y when x² + y² ≤ r²,\n * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,\n * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the\n * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry\n * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc\n * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an\n * odd function.\n *\n * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in\n * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold\n * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight\n * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.\n *\n * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a\n * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the\n * cap and the radius asked for, instead of clipping the disc.\n */\n\nimport { calculatedResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { portableAsin } from \"../core/portable-math.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { calculateDefocusCircle } from \"./depth-of-field.js\";\n\n/**\n * Largest kernel radius in sites of `lens.defocus: { model: \"geometric-disc\" }`, R ≤ 64: a disc of at most 129 sites\n * across (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32). `simulateCapture`,\n * `simulateDisplayView` and `prepareDisplayView` refuse a request whose disc needs more, and never clip the disc; check a\n * request first with {@link calculateDefocusDiscKernelRadius}.\n *\n * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, \"Optics\"): the capture's time grows with\n * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at\n * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus\n * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.\n */\nexport const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;\n\n/** Whether a kernel radius is within {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}: the one test the renderer and the pre-check share. */\nfunction isWithinDefocusDiscKernelLimit(radius: number): boolean {\n return radius <= DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES;\n}\n\n/** The disc sampled on the site lattice. */\nexport interface DefocusDiscSiteKernel {\n /** Disc diameter in sites (c / pitch). */\n readonly diameterSites: number;\n /** Kernel radius R in sites: the window is (2R + 1) × (2R + 1). */\n readonly radius: number;\n /** Window side, 2R + 1. */\n readonly width: number;\n /** Row-major weights, index (j + R)·width + (i + R) for the site offset (i, j); their row-major float64 sum is 1. */\n readonly weights: Float64Array;\n}\n\n/** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */\nexport function defocusDiscKernelRadius(diameterSites: number): number {\n requireDiameter(diameterSites);\n const r = diameterSites / 2;\n return r <= 0.5 ? 0 : Math.ceil(r - 0.5);\n}\n\n/**\n * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)\n * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.\n */\nexport function requireDefocusDiscKernelRadius(diameterSites: number): number {\n const radius = defocusDiscKernelRadius(diameterSites);\n if (!isWithinDefocusDiscKernelLimit(radius))\n throw new InvalidScientificInputError(\"The defocus disc of \" + diameterSites + \" sites needs a kernel radius of \" + radius +\n \" sites; the limit is \" + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + \" sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).\");\n return radius;\n}\n\nfunction requireDiameter(diameterSites: number): void {\n if (typeof diameterSites !== \"number\" || !Number.isFinite(diameterSites) || diameterSites < 0)\n throw new InvalidScientificInputError(\"The defocus disc diameter must be a finite number of sites, zero or more.\");\n}\n\n/** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */\nfunction quadrantArea(x: number, y: number, r: number): number {\n if (x > r) x = r;\n if (y > r) y = r;\n if (x * x + y * y <= r * r) return x * y;\n const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));\n // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².\n return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));\n}\n/** {@link quadrantArea} extended as an odd function of x and of y. */\nfunction signedQuadrantArea(x: number, y: number, r: number): number {\n const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);\n return (x < 0) !== (y < 0) ? -q : q;\n}\n/**\n * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest\n * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference\n * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);\n * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.\n */\nfunction cellArea(i: number, j: number, r: number): number {\n const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;\n const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;\n if (nx * nx + ny * ny >= r * r) return 0;\n const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));\n return area > 0 ? area : 0;\n}\n\n/**\n * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:\n * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when\n * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.\n */\nexport function createDefocusDiscSiteKernel(diameterSites: number): DefocusDiscSiteKernel | null {\n const radius = requireDefocusDiscKernelRadius(diameterSites);\n if (diameterSites === 0) return null;\n const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);\n // One octant, mirrored into the eight symmetric cells.\n for (let i = 0; i <= radius; i++) for (let j = 0; j <= i; j++) {\n const a = cellArea(i, j, r);\n for (const [u, v] of [[i, j], [j, i]] as const) for (const su of u === 0 ? [1] : [1, -1]) for (const sv of v === 0 ? [1] : [1, -1])\n weights[(sv * v + radius) * width + su * u + radius] = a;\n }\n normalize(weights, (width * width - 1) / 2);\n return { diameterSites, radius, width, weights };\n}\n\n/**\n * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a\n * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in\n * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight\n * can move a rounded sum from just below 1 to just above it.)\n */\nfunction normalize(weights: Float64Array, centre: number): void {\n let total = 0;\n for (let k = 0; k < weights.length; k++) total += weights[k]!;\n for (let k = 0; k < weights.length; k++) weights[k]! /= total;\n let hi = 0, lo = 0;\n for (let k = 0; k < weights.length; k++) {\n if (k === centre) continue;\n const w = weights[k]!, s = hi + w, b = s - hi;\n lo += (hi - (s - b)) + (w - b);\n hi = s;\n }\n // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.\n const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);\n weights[centre] = s + (e - lo);\n}\n\n/** The geometric defocus disc of one request, in micrometres and in sites. */\nexport interface ResolvedDefocusDisc {\n /** Blur-circle diameter c at the sensor, µm (`calculateDefocusCircle`, thin-lens model 1.1.0). */\n readonly diameterMicrometers: number;\n /** c / pitch. */\n readonly diameterSites: number;\n /** Kernel radius R in sites (0 when c = 0). */\n readonly kernelRadiusSites: number;\n}\n\n/**\n * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a\n * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.\n */\nexport function resolveDefocusDisc(input: { focalLengthMm: number; aperture: number; focusDistanceM: number; subjectDistanceM: number;\n pitchMicrometers: number }): ResolvedDefocusDisc {\n const { pitchMicrometers, ...circle } = input;\n if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0) throw new InvalidScientificInputError(\"pitchMicrometers must be a positive finite number.\");\n const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;\n const diameterSites = diameterMicrometers / pitchMicrometers;\n return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };\n}\n\nexport interface CalculateDefocusDiscKernelRadiusInput {\n /** Lens focal length in millimetres (`lens.focalLengthMm`). */\n focalLengthMm: number;\n /** F-number the capture uses: `lens.fNumber`, or in an automatic exposure mode the resolved `exposure.settings.fNumber`. */\n aperture: number;\n /** Focus-plane distance from the lens plane in metres (`lens.focusDistanceM`; `simulateCapture` defaults it to the subject distance). */\n focusDistanceM: number;\n /** Scene-plane distance from the lens plane in metres (`subjectDistanceM`). */\n subjectDistanceM: number;\n /** Sensor site pitch in micrometres (`sensor.pixelPitchMicrometers`), the lattice the disc is sampled on. */\n pixelPitchMicrometers: number;\n}\n\nexport interface DefocusDiscKernelRadius {\n /** Geometric blur-circle diameter c at the sensor in micrometres (`calculateDefocusCircle`, thin-lens model 1.1.0); 0 in focus. */\n diameterMicrometers: number;\n /** c / pixel pitch: the disc's diameter in sites. */\n diameterSites: number;\n /** Kernel radius R in sites the renderer needs for this disc, ⌈c / (2·pitch) − ½⌉ (0 for a disc inside one site, and in focus). */\n kernelRadiusSites: number;\n /** {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}, 64. */\n maximumKernelRadiusSites: number;\n /** Largest disc diameter in sites within the limit, 2·64 + 1 = 129: within the limit exactly when `diameterSites` ≤ 129. */\n maximumDiameterSites: number;\n /** True when `kernelRadiusSites` ≤ `maximumKernelRadiusSites`: `lens.defocus` renders the request; false: it refuses it. */\n withinLimit: boolean;\n /** How far past the limit the kernel radius is, max(0, R − 64) sites; 0 when within the limit. */\n excessKernelRadiusSites: number;\n}\n\n/**\n * Checks, before rendering, whether `lens.defocus: { model: \"geometric-disc\" }` can render a request: the geometric\n * defocus disc's diameter on the site lattice, the kernel radius it needs, and whether that radius is within\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} (#532). Pure and cheap: no kernel is built and nothing is rendered.\n *\n * It runs the same resolution and the same limit test as `simulateCapture`, `simulateDisplayView` and `prepareDisplayView`\n * (the blur circle c = f²·|s − d| / (N·d·(s − f)) of `calculateDefocusCircle`, thin-lens model 1.1.0, divided by the pitch),\n * so for the same focal length, f-number, distances and pitch a request they refuse for its defocus size has\n * `withinLimit: false`, and one they accept has `withinLimit: true`. Pass the values the capture uses: in an automatic\n * exposure mode that is the resolved f-number. The limit applies to the disc alone; with `lens.diffraction` the combined\n * kernel (diffraction radius plus defocus radius, at most 32 + 64 sites) has no further limit. Other reasons a capture\n * refuses `lens.defocus` out of focus (camera rotation, projection, distortion, vignetting, pupil sampling, a caller PSF)\n * are not checked here.\n *\n * The blur circle is a geometric thin-lens approximation for an ideal uniform circular pupil (no diffraction, aberrations\n * or pupil vignetting). Inputs follow `calculateDefocusCircle`: all positive and finite, both distances beyond the focal\n * length; invalid values fail with `InvalidScientificInputError`, unknown fields with `InvalidConfigurationError`.\n *\n * @param input Lens, f-number, focus and subject distances, and the sensor's site pitch.\n * @returns The disc and its kernel radius, the limit, and whether the request is within it and by how much it is not.\n */\nexport function calculateDefocusDiscKernelRadius(input: CalculateDefocusDiscKernelRadiusInput): CalculationResult<DefocusDiscKernelRadius> {\n requireInputKeys(input, \"calculateDefocusDiscKernelRadius\", [\"focalLengthMm\", \"aperture\", \"focusDistanceM\", \"subjectDistanceM\", \"pixelPitchMicrometers\"]);\n requirePositiveFinite(\"pixelPitchMicrometers\", input.pixelPitchMicrometers);\n const disc = resolveDefocusDisc({ focalLengthMm: input.focalLengthMm, aperture: input.aperture, focusDistanceM: input.focusDistanceM,\n subjectDistanceM: input.subjectDistanceM, pitchMicrometers: input.pixelPitchMicrometers });\n const withinLimit = isWithinDefocusDiscKernelLimit(disc.kernelRadiusSites);\n return calculatedResult({\n diameterMicrometers: disc.diameterMicrometers,\n diameterSites: disc.diameterSites,\n kernelRadiusSites: disc.kernelRadiusSites,\n maximumKernelRadiusSites: DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES,\n maximumDiameterSites: 2 * DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + 1,\n withinLimit,\n excessKernelRadiusSites: withinLimit ? 0 : disc.kernelRadiusSites - DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES\n }, \"defocus-disc-kernel-radius\", \"1.0.0\", [\n \"Geometric thin-lens blur circle of an ideal uniform circular pupil (thin-lens-defocus-circle 1.1.0), an approximation\",\n \"The disc and limit test lens.defocus { model: \\\"geometric-disc\\\" } applies in simulateCapture and the display view\",\n \"Only the defocus size limit is checked\"\n ]);\n}\n\n/**\n * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius\n * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps\n * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral\n * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF\n * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are\n * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.\n */\nexport function convolveSiteKernels(a: Float64Array, radiusA: number, b: Float64Array, radiusB: number): Float64Array {\n const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;\n if (a.length !== wa * wa || b.length !== wb * wb) throw new InvalidScientificInputError(\"Site kernels must be (2R + 1)² taps.\");\n const out = new Float64Array(width * width);\n // Non-zero taps of b, as (row, column, weight).\n const bRows: number[] = [], bColumns: number[] = [], bWeights: number[] = [];\n for (let j = 0; j < wb; j++) for (let i = 0; i < wb; i++) { const w = b[j * wb + i]!; if (w !== 0) { bRows.push(j); bColumns.push(i); bWeights.push(w); } }\n for (let sj = 0; sj < wa; sj++) for (let si = 0; si < wa; si++) {\n const w = a[sj * wa + si]!;\n if (w === 0) continue;\n for (let k = 0; k < bWeights.length; k++) {\n const index = (sj + bRows[k]!) * width + si + bColumns[k]!;\n out[index] = out[index]! + w * bWeights[k]!;\n }\n }\n return out;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"defocus-disc-kernel.js","sourceRoot":"","sources":["../../src/optics/defocus-disc-kernel.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The geometric defocus disc of a flat scene, sampled on the square site lattice, for\n * `lens.defocus: { model: \"geometric-disc\" }` (#492). Internal except for the radius cap\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} and the pre-check {@link calculateDefocusDiscKernelRadius}, which are\n * root exports (#532), so a caller can check a request against the cap before rendering.\n *\n * Model. For a scene plane at the subject distance d and a thin lens focused at s, an ideal uniform circular pupil\n * spreads every in-focus image point into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor\n * (`calculateDefocusCircle`, thin-lens model 1.1.0; geometric optics, no diffraction or aberrations). The defocused\n * image of the plane is therefore the in-focus image convolved with that disc: one fixed, wavelength-independent\n * kernel. It is an approximation (geometric optics, ideal uniform circular pupil, no vignetting of the pupil).\n *\n * Sampling. The disc of diameter c/p sites (p the site pitch) is centred on a site centre. The weight of the site at\n * offset (i, j) is the exact area of the disc inside that site's square cell [i − ½, i + ½] × [j − ½, j + ½], divided\n * by the area of the disc. The area of a disc inside a rectangle is a signed sum over the rectangle's four corners of\n * the area Q(x, y) of the disc inside the quadrant [0, x] × [0, y] (inclusion–exclusion), and for x, y ≥ 0 with\n * x, y clamped to the radius r\n * Q = x·y when x² + y² ≤ r²,\n * Q = ½·(x_a·y + x·y_a + r²·(arcsin(x/r) − arcsin(x_a/r))) otherwise,\n * with x_a = √(r² − y²) and y_a = √(r² − x²) the arc's crossings of the quadrant's edges: the rectangle left of the\n * crossing plus the area under the arc, ∫ √(r² − t²) dt = ½·(t·√(r² − t²) + r²·arcsin(t/r)) (elementary geometry\n * and calculus; NIST DLMF 4.24.1 for the arcsine, free). The arcsine difference is the angle between the two arc\n * points, evaluated as arcsin((x·y − x_a·y_a)/r²) (the sine of a difference). Q is extended to negative x or y as an\n * odd function.\n *\n * Determinism. Only correctly rounded operations and `portableAsin` are used, so the weights are the same bits in\n * every JavaScript engine. One octant (0 ≤ j ≤ i) is computed and mirrored, so the kernel has the exact 8-fold\n * symmetry of the disc and the square lattice. The weights are divided by their float64 sum, then the centre weight\n * takes up the remaining rounding, so their exact sum is 1 within half a unit in the last place of the centre weight.\n *\n * Limits. The window is (2R + 1)² sites with R = ⌈r − ½⌉ (0 when r ≤ ½: a disc inside one cell is the identity, a\n * valid kernel). R is capped at {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}; past it the call fails, naming the\n * cap and the radius asked for, instead of clipping the disc.\n */\n\nimport { calculatedResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { portableAsin } from \"../core/portable-math.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidScientificInputError, requirePositiveFinite } from \"../core/validation.js\";\nimport { calculateDefocusCircle } from \"./depth-of-field.js\";\n\n/**\n * Largest kernel radius in sites of `lens.defocus: { model: \"geometric-disc\" }`, R ≤ 64: a disc of at most 129 sites\n * across (a 129 × 129 window, 16 641 taps per site; the diffraction kernels stop at 32). `simulateCapture`,\n * `simulateDisplayView` and `prepareDisplayView` refuse a request whose disc needs more, and never clip the disc; check a\n * request first with {@link calculateDefocusDiscKernelRadius}.\n *\n * Set from the cost measured in the capture (#492, step 2a; docs/QUICK_START.md, \"Optics\"): the capture's time grows with\n * R² at every site the disc sees an edge from, and a 0.7 MP Siemens star took 0.37 s in focus, 2.9 s at R = 32, 4.7 s at\n * R = 41 and 11.6 s at R = 64 (Node, one thread). 64 keeps the issue's 1.4 µm example (50 mm f/2.8, subject 2 m, focus\n * 1.6 m: R = 41) with room for larger blurs, at a cost the caller chooses and the docs state; past it the call fails.\n */\nexport const DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES = 64;\n\n/** Whether a kernel radius is within {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}: the one test the renderer and the pre-check share. */\nfunction isWithinDefocusDiscKernelLimit(radius: number): boolean {\n return radius <= DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES;\n}\n\n/** The disc sampled on the site lattice. */\nexport interface DefocusDiscSiteKernel {\n /** Disc diameter in sites (c / pitch). */\n readonly diameterSites: number;\n /** Kernel radius R in sites: the window is (2R + 1) × (2R + 1). */\n readonly radius: number;\n /** Window side, 2R + 1. */\n readonly width: number;\n /** Row-major weights, index (j + R)·width + (i + R) for the site offset (i, j); their row-major float64 sum is 1. */\n readonly weights: Float64Array;\n}\n\n/** Kernel radius in sites for a disc of `diameterSites`: the cells [i − ½, i + ½] the disc reaches with positive area. */\nexport function defocusDiscKernelRadius(diameterSites: number): number {\n requireDiameter(diameterSites);\n const r = diameterSites / 2;\n return r <= 0.5 ? 0 : Math.ceil(r - 0.5);\n}\n\n/**\n * {@link defocusDiscKernelRadius}, failing with `InvalidScientificInputError` (naming the cap and the radius asked for)\n * when it exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}. The disc is never clipped to fit.\n */\nexport function requireDefocusDiscKernelRadius(diameterSites: number): number {\n const radius = defocusDiscKernelRadius(diameterSites);\n if (!isWithinDefocusDiscKernelLimit(radius))\n throw new InvalidScientificInputError(\"The defocus disc of \" + diameterSites + \" sites needs a kernel radius of \" + radius +\n \" sites; the limit is \" + DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + \" sites (DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES).\");\n return radius;\n}\n\nfunction requireDiameter(diameterSites: number): void {\n if (typeof diameterSites !== \"number\" || !Number.isFinite(diameterSites) || diameterSites < 0)\n throw new InvalidScientificInputError(\"The defocus disc diameter must be a finite number of sites, zero or more.\");\n}\n\n/** Area of the disc of radius r (centred at the origin) inside [0, x] × [0, y], for x, y ≥ 0. */\nfunction quadrantArea(x: number, y: number, r: number): number {\n if (x > r) x = r;\n if (y > r) y = r;\n if (x * x + y * y <= r * r) return x * y;\n const xa = Math.sqrt((r - y) * (r + y)), ya = Math.sqrt((r - x) * (r + x));\n // arcsin(x/r) − arcsin(x_a/r) is the angle between the arc points (x_a, y) and (x, y_a), whose sine is (x·y − x_a·y_a)/r².\n return 0.5 * (xa * y + x * ya + r * r * portableAsin(Math.min(1, (x * y - xa * ya) / (r * r))));\n}\n/** {@link quadrantArea} extended as an odd function of x and of y. */\nfunction signedQuadrantArea(x: number, y: number, r: number): number {\n const q = quadrantArea(x < 0 ? -x : x, y < 0 ? -y : y, r);\n return (x < 0) !== (y < 0) ? -q : q;\n}\n/**\n * Area of the disc inside the cell [i − ½, i + ½] × [j − ½, j + ½], for i ≥ j ≥ 0. Exactly 0 when the cell's point nearest\n * the centre lies on or outside the circle. Otherwise the four corner terms are up to r² in size, so the difference\n * carries an absolute rounding error of a few r²·2^-53 (below 10^-11 at the radius cap, under 10^-15 of the disc's area);\n * a cell that barely touches the disc can come out a rounding error below zero and is set to 0.\n */\nfunction cellArea(i: number, j: number, r: number): number {\n const x0 = i - 0.5, x1 = i + 0.5, y0 = j - 0.5, y1 = j + 0.5;\n const nx = i === 0 ? 0 : x0, ny = j === 0 ? 0 : y0;\n if (nx * nx + ny * ny >= r * r) return 0;\n const area = (signedQuadrantArea(x1, y1, r) - signedQuadrantArea(x0, y1, r)) - (signedQuadrantArea(x1, y0, r) - signedQuadrantArea(x0, y0, r));\n return area > 0 ? area : 0;\n}\n\n/**\n * The defocus disc of `diameterSites` (c / pitch) sampled on the site lattice, or null for a zero diameter (in focus:\n * callers apply no kernel). Fails with `InvalidScientificInputError` for a negative or non-finite diameter, and when\n * the kernel radius exceeds {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}.\n */\nexport function createDefocusDiscSiteKernel(diameterSites: number): DefocusDiscSiteKernel | null {\n const radius = requireDefocusDiscKernelRadius(diameterSites);\n if (diameterSites === 0) return null;\n const r = diameterSites / 2, width = 2 * radius + 1, weights = new Float64Array(width * width);\n // One octant, mirrored into the eight symmetric cells.\n for (let i = 0; i <= radius; i++) for (let j = 0; j <= i; j++) {\n const a = cellArea(i, j, r);\n for (const [u, v] of [[i, j], [j, i]] as const) for (const su of u === 0 ? [1] : [1, -1]) for (const sv of v === 0 ? [1] : [1, -1])\n weights[(sv * v + radius) * width + su * u + radius] = a;\n }\n normalize(weights, (width * width - 1) / 2);\n return { diameterSites, radius, width, weights };\n}\n\n/**\n * Divides by the float64 sum, then sets the centre weight to 1 minus the exact sum of the others (accumulated as a\n * double-double with Knuth's TwoSum), rounded once. The exact real sum of the weights is then 1 within half a unit in\n * the last place of the centre weight. (No float64 summation order can promise exactly 1: a one-ulp change of one weight\n * can move a rounded sum from just below 1 to just above it.)\n */\nfunction normalize(weights: Float64Array, centre: number): void {\n let total = 0;\n for (let k = 0; k < weights.length; k++) total += weights[k]!;\n for (let k = 0; k < weights.length; k++) weights[k]! /= total;\n let hi = 0, lo = 0;\n for (let k = 0; k < weights.length; k++) {\n if (k === centre) continue;\n const w = weights[k]!, s = hi + w, b = s - hi;\n lo += (hi - (s - b)) + (w - b);\n hi = s;\n }\n // 1 − (hi + lo): TwoSum of 1 and −hi, then the low parts.\n const s = 1 - hi, b = s - 1, e = (1 - (s - b)) + (-hi - b);\n weights[centre] = s + (e - lo);\n}\n\n/** The geometric defocus disc of one request, in micrometres and in sites. */\nexport interface ResolvedDefocusDisc {\n /** Blur-circle diameter c at the sensor, µm (`calculateDefocusCircle`, thin-lens model 1.1.0). */\n readonly diameterMicrometers: number;\n /** c / pitch. */\n readonly diameterSites: number;\n /** Kernel radius R in sites (0 when c = 0). */\n readonly kernelRadiusSites: number;\n}\n\n/**\n * Resolves the defocus disc for a thin lens focused at `focusDistanceM` imaging a plane at `subjectDistanceM`, on a\n * lattice of `pitchMicrometers`. Exactly zero when the two distances are equal. It does not check the radius cap.\n */\nexport function resolveDefocusDisc(input: { focalLengthMm: number; aperture: number; focusDistanceM: number; subjectDistanceM: number;\n pitchMicrometers: number }): ResolvedDefocusDisc {\n const { pitchMicrometers, ...circle } = input;\n if (!Number.isFinite(pitchMicrometers) || pitchMicrometers <= 0) throw new InvalidScientificInputError(\"pitchMicrometers must be a positive finite number.\");\n const diameterMicrometers = calculateDefocusCircle(circle).value.diameterMm * 1000;\n const diameterSites = diameterMicrometers / pitchMicrometers;\n return { diameterMicrometers, diameterSites, kernelRadiusSites: defocusDiscKernelRadius(diameterSites) };\n}\n\nexport interface CalculateDefocusDiscKernelRadiusInput {\n /** Lens focal length in millimetres (`lens.focalLengthMm`). */\n focalLengthMm: number;\n /** F-number the capture uses: `lens.fNumber`, or in an automatic exposure mode the resolved `exposure.settings.fNumber`. */\n aperture: number;\n /** Focus-plane distance from the lens plane in metres (`lens.focusDistanceM`; `simulateCapture` defaults it to the subject distance). */\n focusDistanceM: number;\n /** Scene-plane distance from the lens plane in metres (`subjectDistanceM`). */\n subjectDistanceM: number;\n /** Sensor site pitch in micrometres (`sensor.pixelPitchMicrometers`), the lattice the disc is sampled on. */\n pixelPitchMicrometers: number;\n}\n\nexport interface DefocusDiscKernelRadius {\n /** Geometric blur-circle diameter c at the sensor in micrometres (`calculateDefocusCircle`, thin-lens model 1.1.0); 0 in focus. */\n diameterMicrometers: number;\n /** c / pixel pitch: the disc's diameter in sites. */\n diameterSites: number;\n /** Kernel radius R in sites the renderer needs for this disc, ⌈c / (2·pitch) − ½⌉ (0 for a disc inside one site, and in focus). */\n kernelRadiusSites: number;\n /** {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES}, 64. */\n maximumKernelRadiusSites: number;\n /** Largest disc diameter in sites within the limit, 2·64 + 1 = 129: within the limit exactly when `diameterSites` ≤ 129. */\n maximumDiameterSites: number;\n /** True when `kernelRadiusSites` ≤ `maximumKernelRadiusSites`: `lens.defocus` renders the request; false: it refuses it. */\n withinLimit: boolean;\n /** How far past the limit the kernel radius is, max(0, R − 64) sites; 0 when within the limit. */\n excessKernelRadiusSites: number;\n}\n\n/**\n * Checks, before rendering, whether `lens.defocus: { model: \"geometric-disc\" }` can render a request: the geometric\n * defocus disc's diameter on the site lattice, the kernel radius it needs, and whether that radius is within\n * {@link DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES} (#532). Pure and cheap: no kernel is built and nothing is rendered.\n *\n * It runs the same resolution and the same limit test as `simulateCapture`, `simulateDisplayView` and `prepareDisplayView`\n * (the blur circle c = f²·|s − d| / (N·d·(s − f)) of `calculateDefocusCircle`, thin-lens model 1.1.0, divided by the pitch),\n * so for the same focal length, f-number, distances and pitch a request they refuse for its defocus size has\n * `withinLimit: false`, and one they accept has `withinLimit: true`. Pass the values the capture uses: in an automatic\n * exposure mode that is the resolved f-number. The limit applies to the disc alone; with `lens.diffraction` the combined\n * kernel (diffraction radius plus defocus radius, at most 32 + 64 sites) has no further limit. Other reasons a capture\n * refuses `lens.defocus` out of focus (pupil sampling, a caller PSF, camera rotation with a rolling shutter, a vignetting or\n * distortion profile that does not reach the kernel's radius beyond the frame, and in the display view a projection,\n * distortion, vignetting or rotation) are not checked here.\n *\n * The blur circle is a geometric thin-lens approximation for an ideal uniform circular pupil (no diffraction, aberrations\n * or pupil vignetting). Inputs follow `calculateDefocusCircle`: all positive and finite, both distances beyond the focal\n * length; invalid values fail with `InvalidScientificInputError`, unknown fields with `InvalidConfigurationError`.\n *\n * @param input Lens, f-number, focus and subject distances, and the sensor's site pitch.\n * @returns The disc and its kernel radius, the limit, and whether the request is within it and by how much it is not.\n */\nexport function calculateDefocusDiscKernelRadius(input: CalculateDefocusDiscKernelRadiusInput): CalculationResult<DefocusDiscKernelRadius> {\n requireInputKeys(input, \"calculateDefocusDiscKernelRadius\", [\"focalLengthMm\", \"aperture\", \"focusDistanceM\", \"subjectDistanceM\", \"pixelPitchMicrometers\"]);\n requirePositiveFinite(\"pixelPitchMicrometers\", input.pixelPitchMicrometers);\n const disc = resolveDefocusDisc({ focalLengthMm: input.focalLengthMm, aperture: input.aperture, focusDistanceM: input.focusDistanceM,\n subjectDistanceM: input.subjectDistanceM, pitchMicrometers: input.pixelPitchMicrometers });\n const withinLimit = isWithinDefocusDiscKernelLimit(disc.kernelRadiusSites);\n return calculatedResult({\n diameterMicrometers: disc.diameterMicrometers,\n diameterSites: disc.diameterSites,\n kernelRadiusSites: disc.kernelRadiusSites,\n maximumKernelRadiusSites: DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES,\n maximumDiameterSites: 2 * DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES + 1,\n withinLimit,\n excessKernelRadiusSites: withinLimit ? 0 : disc.kernelRadiusSites - DEFOCUS_DISC_KERNEL_MAXIMUM_RADIUS_SITES\n }, \"defocus-disc-kernel-radius\", \"1.0.0\", [\n \"Geometric thin-lens blur circle of an ideal uniform circular pupil (thin-lens-defocus-circle 1.1.0), an approximation\",\n \"The disc and limit test lens.defocus { model: \\\"geometric-disc\\\" } applies in simulateCapture and the display view\",\n \"Only the defocus size limit is checked\"\n ]);\n}\n\n/**\n * The full 2-D convolution of two square site kernels (row-major, radii `radiusA` and `radiusB`): a kernel of radius\n * radiusA + radiusB with out(t) = Σ_s a(s)·b(t − s), each output summed over the taps of `a` in row-major order (zero taps\n * of either kernel skipped, which adds exact zeros only). Used for lens.defocus with lens.diffraction (#492): each spectral\n * node's diffraction kernel convolved with the defocus disc. That combination is an approximation: the defocused wave PSF\n * of a circular pupil is not exactly the in-focus Airy pattern convolved with the geometric disc. Both inputs here are\n * centred and non-negative, so the result is too; its sum is the product of the inputs' sums up to rounding.\n */\nexport function convolveSiteKernels(a: Float64Array, radiusA: number, b: Float64Array, radiusB: number): Float64Array {\n const wa = 2 * radiusA + 1, wb = 2 * radiusB + 1, radius = radiusA + radiusB, width = 2 * radius + 1;\n if (a.length !== wa * wa || b.length !== wb * wb) throw new InvalidScientificInputError(\"Site kernels must be (2R + 1)² taps.\");\n const out = new Float64Array(width * width);\n // Non-zero taps of b, as (row, column, weight).\n const bRows: number[] = [], bColumns: number[] = [], bWeights: number[] = [];\n for (let j = 0; j < wb; j++) for (let i = 0; i < wb; i++) { const w = b[j * wb + i]!; if (w !== 0) { bRows.push(j); bColumns.push(i); bWeights.push(w); } }\n for (let sj = 0; sj < wa; sj++) for (let si = 0; si < wa; si++) {\n const w = a[sj * wa + si]!;\n if (w === 0) continue;\n for (let k = 0; k < bWeights.length; k++) {\n const index = (sj + bRows[k]!) * width + si + bColumns[k]!;\n out[index] = out[index]! + w * bWeights[k]!;\n }\n }\n return out;\n}\n"]}
|
|
@@ -118,6 +118,37 @@ export interface SensorEnvironmentRadianceQuery {
|
|
|
118
118
|
* computes radiance or renders.
|
|
119
119
|
*/
|
|
120
120
|
export declare function calculateSensorEnvironmentRadianceQuery(input: CalculateSensorEnvironmentRadianceQueryInput): CalculationResult<SensorEnvironmentRadianceQuery>;
|
|
121
|
+
/**
|
|
122
|
+
* @internal The ideal (undistorted) image point of a physical sensor point (+X right, +Y up, mm) and the distortion area
|
|
123
|
+
* factor 1 / det(d distorted / d ideal) there; the ideal point is the sensor point itself without distortion. Shared by
|
|
124
|
+
* the mapped query below and the image-space defocus of `simulateCapture` (#548), so both use one arithmetic.
|
|
125
|
+
*/
|
|
126
|
+
export declare function undistortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, imagePointMm: {
|
|
127
|
+
x: number;
|
|
128
|
+
y: number;
|
|
129
|
+
}): {
|
|
130
|
+
ideal: {
|
|
131
|
+
x: number;
|
|
132
|
+
y: number;
|
|
133
|
+
};
|
|
134
|
+
distortionAreaFactor: number;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* @internal The forward distortion of an ideal image point (+X right, +Y up, mm) to the physical sensor point, with the
|
|
138
|
+
* singular values of d distorted / d ideal there (the largest and smallest local scale of the map; 1 and 1 without
|
|
139
|
+
* distortion). The inverse of {@link undistortedImagePoint} up to its solver's rounding (#548).
|
|
140
|
+
*/
|
|
141
|
+
export declare function distortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, ideal: {
|
|
142
|
+
x: number;
|
|
143
|
+
y: number;
|
|
144
|
+
}): {
|
|
145
|
+
pointMm: {
|
|
146
|
+
x: number;
|
|
147
|
+
y: number;
|
|
148
|
+
};
|
|
149
|
+
largestScale: number;
|
|
150
|
+
smallestScale: number;
|
|
151
|
+
};
|
|
121
152
|
/**
|
|
122
153
|
* @internal Query with a lens field mapping that is already validated. The environment photo-signal
|
|
123
154
|
* planner shares it, so profiles are validated once per plan.
|
|
@@ -106,21 +106,15 @@ export function calculateSensorEnvironmentRadianceQuery(input) {
|
|
|
106
106
|
]);
|
|
107
107
|
}
|
|
108
108
|
/**
|
|
109
|
-
* @internal
|
|
110
|
-
*
|
|
109
|
+
* @internal The ideal (undistorted) image point of a physical sensor point (+X right, +Y up, mm) and the distortion area
|
|
110
|
+
* factor 1 / det(d distorted / d ideal) there; the ideal point is the sensor point itself without distortion. Shared by
|
|
111
|
+
* the mapped query below and the image-space defocus of `simulateCapture` (#548), so both use one arithmetic.
|
|
111
112
|
*/
|
|
112
|
-
export function
|
|
113
|
-
|
|
114
|
-
input.environmentDirectionConvention !== "outgoing-radiance-toward-camera") {
|
|
115
|
-
throw new InvalidScientificInputError("Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.");
|
|
116
|
-
}
|
|
117
|
-
const focus = parseFocusPlane(input.focus);
|
|
118
|
-
const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };
|
|
119
|
-
const mapping = prepared.mapping;
|
|
120
|
-
let ideal = sourcePointImagePlaneMm;
|
|
113
|
+
export function undistortedImagePoint(prepared, imagePointMm) {
|
|
114
|
+
let ideal = imagePointMm;
|
|
121
115
|
let distortionAreaFactor = 1;
|
|
122
116
|
if (prepared.radial !== undefined) {
|
|
123
|
-
ideal = calculateInverseRadialSourcePointValue(
|
|
117
|
+
ideal = calculateInverseRadialSourcePointValue(imagePointMm, prepared.radial, "sourcePointNativeSensorMm").sourceImagePointMm;
|
|
124
118
|
// Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).
|
|
125
119
|
const { k1, k2, k3 } = prepared.radial.coefficients;
|
|
126
120
|
const r2 = (ideal.x * ideal.x + ideal.y * ideal.y) / prepared.radial.normalizationRadiusMm ** 2;
|
|
@@ -129,13 +123,51 @@ export function calculateMappedSensorEnvironmentRadianceQuery(input, prepared) {
|
|
|
129
123
|
distortionAreaFactor = 1 / (scale * radialDerivative);
|
|
130
124
|
}
|
|
131
125
|
else if (prepared.brownConrady !== undefined) {
|
|
132
|
-
ideal = brownConradyInverseValue(prepared.brownConrady,
|
|
126
|
+
ideal = brownConradyInverseValue(prepared.brownConrady, imagePointMm);
|
|
133
127
|
const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;
|
|
134
128
|
distortionAreaFactor = 1 / (j[0] * j[3] - j[1] * j[2]);
|
|
135
129
|
}
|
|
136
130
|
if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {
|
|
137
131
|
throw new InvalidScientificInputError("Distortion area factor must remain finite and positive.");
|
|
138
132
|
}
|
|
133
|
+
return { ideal, distortionAreaFactor };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* @internal The forward distortion of an ideal image point (+X right, +Y up, mm) to the physical sensor point, with the
|
|
137
|
+
* singular values of d distorted / d ideal there (the largest and smallest local scale of the map; 1 and 1 without
|
|
138
|
+
* distortion). The inverse of {@link undistortedImagePoint} up to its solver's rounding (#548).
|
|
139
|
+
*/
|
|
140
|
+
export function distortedImagePoint(prepared, ideal) {
|
|
141
|
+
if (prepared.radial !== undefined) {
|
|
142
|
+
const { k1, k2, k3 } = prepared.radial.coefficients;
|
|
143
|
+
const r2 = (ideal.x * ideal.x + ideal.y * ideal.y) / prepared.radial.normalizationRadiusMm ** 2;
|
|
144
|
+
const scale = 1 + k1 * r2 + k2 * r2 * r2 + k3 * r2 * r2 * r2;
|
|
145
|
+
// Tangential scale s, radial scale s + r ds/dr (the two eigenvalues of the symmetric Jacobian of p -> s(r) p).
|
|
146
|
+
const radialDerivative = 1 + 3 * k1 * r2 + 5 * k2 * r2 * r2 + 7 * k3 * r2 * r2 * r2;
|
|
147
|
+
return { pointMm: { x: ideal.x * scale, y: ideal.y * scale }, largestScale: Math.max(scale, radialDerivative), smallestScale: Math.min(scale, radialDerivative) };
|
|
148
|
+
}
|
|
149
|
+
if (prepared.brownConrady !== undefined) {
|
|
150
|
+
const forward = brownConradyForwardValue(prepared.brownConrady, ideal), [a, b, c, d] = forward.jacobian;
|
|
151
|
+
// Singular values of [[a, b], [c, d]]: sqrt of the eigenvalues of J^T J.
|
|
152
|
+
const p = a * a + b * b + c * c + d * d, q = Math.abs(a * d - b * c), root = Math.sqrt(Math.max(0, p * p - 4 * q * q));
|
|
153
|
+
const largest = Math.sqrt((p + root) / 2);
|
|
154
|
+
return { pointMm: forward.mappedImagePointMm, largestScale: largest, smallestScale: largest > 0 ? q / largest : 0 };
|
|
155
|
+
}
|
|
156
|
+
return { pointMm: { ...ideal }, largestScale: 1, smallestScale: 1 };
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* @internal Query with a lens field mapping that is already validated. The environment photo-signal
|
|
160
|
+
* planner shares it, so profiles are validated once per plan.
|
|
161
|
+
*/
|
|
162
|
+
export function calculateMappedSensorEnvironmentRadianceQuery(input, prepared) {
|
|
163
|
+
if (input.timeReference !== "first-opening-boundary-phase" ||
|
|
164
|
+
input.environmentDirectionConvention !== "outgoing-radiance-toward-camera") {
|
|
165
|
+
throw new InvalidScientificInputError("Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.");
|
|
166
|
+
}
|
|
167
|
+
const focus = parseFocusPlane(input.focus);
|
|
168
|
+
const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };
|
|
169
|
+
const mapping = prepared.mapping;
|
|
170
|
+
const { ideal, distortionAreaFactor } = undistortedImagePoint(prepared, sourcePointImagePlaneMm);
|
|
139
171
|
let projection = null;
|
|
140
172
|
let look;
|
|
141
173
|
let projectionScaleMm;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal 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
|
+
{"version":3,"file":"sensor-environment-query.js","sourceRoot":"","sources":["../../src/optics/sensor-environment-query.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Builds an environment radiance request from points on the physical sensor. It uses the existing\n * focus-aware ideal projection and the analytic inverse camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The direction sent to the provider points opposite the\n * look ray at exposure start, because radiance travels toward the camera. This module does not\n * intersect the scene, compute radiance or render anything.\n *\n * Optional lens field mapping (2.0):\n * 1. The physical sensor point is inverse-distorted (radial or Brown-Conrady) to the ideal image\n * point.\n * 2. That point is unprojected with the declared ideal projection (rectilinear, fisheye or\n * equirectangular), at the same focus-aware projection scale.\n * 3. The ray is rotated to the camera axes at exposure start.\n * The local area factor 1 / det(d distorted / d ideal) is reported. The caller applies it to\n * irradiance exactly once. Without the mapping, the existing rectilinear computation is unchanged.\n * @see docs/PHYSICS_FOUNDATION.md for the equations, units, coordinate conventions, known blockers\n * and support limits.\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { applyPhysicalCameraRotation, calculateInverseCameraRotationImageMapping,\n type CameraAngularVelocityRadPerSec } from \"../motion/camera-rotation.js\";\nimport { lensProjectionDirectionValue, parseLensProjectionModel, type LensProjectionModel } from \"../camera/lens-projection.js\";\nimport {\n calculateInverseRadialSourcePointValue, validateRadialDistortionProfile, type RadialDistortionProfile, type ValidatedRadialProfile\n} from \"./radial-distortion.js\";\nimport {\n brownConradyForwardValue, brownConradyInverseValue, validateBrownConradyDistortionProfile,\n type BrownConradyDistortionProfile, type ValidatedBrownConradyDistortionProfile\n} from \"./brown-conrady-distortion.js\";\nimport { calculateThinLensImageDistance } from \"./thin-lens.js\";\nimport { parseSceneRadianceEvaluationRequest, type SceneRadianceEvaluationRequest } from \"../schema/scene-radiance.js\";\nimport { parseFocusPlane, type FocusPlane } from \"./focus-state.js\";\nimport { requireInputKeys } from \"../core/record-validation.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { hypot3 } from \"../core/portable-math.js\";\n\n/**\n * Lens field mapping between the physical (distorted) sensor point and the scene ray (2.0).\n * `radiometricAreaCorrection` states who applies the distortion area factor. The environment\n * photo signal multiplies irradiance by 1 / det(d distorted / d ideal) exactly once. A supplied\n * field-throughput profile must therefore not include it as well.\n */\nexport interface EnvironmentLensFieldMapping {\n projectionModel: LensProjectionModel;\n distortion:\n | { kind: \"none\" }\n | { kind: \"radial\"; profile: RadialDistortionProfile }\n | { kind: \"brown-conrady\"; profile: BrownConradyDistortionProfile };\n radiometricAreaCorrection: \"inverse-distortion-jacobian\";\n}\n\n/** @internal Parsed mapping with validated profiles, reused across one plan's queries. */\nexport interface PreparedEnvironmentLensFieldMapping {\n mapping: EnvironmentLensFieldMapping;\n radial?: ValidatedRadialProfile;\n brownConrady?: ValidatedBrownConradyDistortionProfile;\n}\n\n/** @internal Strict parse and validation of a lens field mapping. Unknown fields are rejected. */\nexport function prepareEnvironmentLensFieldMapping(value: unknown): PreparedEnvironmentLensFieldMapping {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) {\n throw new InvalidConfigurationError(\"lensFieldMapping must be an object.\");\n }\n const record = value as Record<string, unknown>;\n for (const key of Object.keys(record)) {\n if (![\"projectionModel\", \"distortion\", \"radiometricAreaCorrection\"].includes(key)) {\n throw new InvalidConfigurationError(`lensFieldMapping has unknown field ${JSON.stringify(key)}.`);\n }\n }\n if (record.radiometricAreaCorrection !== \"inverse-distortion-jacobian\") {\n throw new InvalidConfigurationError(\"lensFieldMapping.radiometricAreaCorrection must be \\\"inverse-distortion-jacobian\\\".\");\n }\n const projectionModel = parseLensProjectionModel(record.projectionModel);\n const distortion = record.distortion as EnvironmentLensFieldMapping[\"distortion\"] | undefined;\n if (typeof distortion !== \"object\" || distortion === null) {\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion must be an object.\");\n }\n if (distortion.kind === \"none\") {\n return { mapping: { projectionModel, distortion: { kind: \"none\" }, radiometricAreaCorrection: \"inverse-distortion-jacobian\" } };\n }\n if (distortion.kind === \"radial\") {\n const radial = validateRadialDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"radial\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, radial };\n }\n if (distortion.kind === \"brown-conrady\") {\n const brownConrady = validateBrownConradyDistortionProfile(distortion.profile);\n return { mapping: { projectionModel, distortion: { kind: \"brown-conrady\", profile: distortion.profile },\n radiometricAreaCorrection: \"inverse-distortion-jacobian\" }, brownConrady };\n }\n throw new InvalidConfigurationError(\"lensFieldMapping.distortion.kind must be none, radial or brown-conrady.\");\n}\n\nexport interface CalculateSensorEnvironmentRadianceQueryInput {\n /** Optical sample point before the anti-aliasing filter or PSF. Native +X right, +Y down, in mm from the optical axis. */\n sourcePointNativeSensorMm: { x: number; y: number };\n focalLengthMm: number;\n focus: FocusPlane;\n angularVelocityRadPerSec: CameraAngularVelocityRadPerSec;\n /** States that the moment the shutter opens is t=0 in the rotation model. */\n timeReference: \"first-opening-boundary-phase\";\n timeSecondsFromOpeningReference: number;\n /** The request uses the direction radiance travels: toward the camera, opposite the look ray. */\n environmentDirectionConvention: \"outgoing-radiance-toward-camera\";\n request: Omit<SceneRadianceEvaluationRequest, \"target\" | \"timeSecondsFromExposureStart\">;\n /** Optional 2.0 lens field mapping. Omit it for the ideal rectilinear, distortion-free query. */\n lensFieldMapping?: EnvironmentLensFieldMapping;\n}\n\nexport interface SensorEnvironmentLensFieldMappingResult {\n projectionModel: LensProjectionModel;\n distortionKind: \"none\" | \"radial\" | \"brown-conrady\";\n /** Ideal (undistorted) image-plane point, +X right / +Y up, mm. */\n idealImagePointMm: { x: number; y: number };\n /** Projection scale: thin-lens image distance at finite focus, focal length at infinity, mm. */\n projectionScaleMm: number;\n /** 1 / det(d distorted / d ideal) at this point; 1 without distortion. */\n distortionAreaFactor: number;\n}\n\nexport interface SensorEnvironmentRadianceQuery {\n request: SceneRadianceEvaluationRequest;\n sourcePointNativeSensorMm: { x: number; y: number };\n sourcePointImagePlaneMm: { x: number; y: number };\n referenceLookDirectionUnitVector: { x: number; y: number; z: number };\n /** Rectilinear rotation mapping. Null when a non-rectilinear lens field mapping is used. */\n projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null;\n /** Present only when the input declared a lens field mapping. */\n lensFieldMapping?: SensorEnvironmentLensFieldMappingResult;\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\";\n directionConvention: \"outgoing-radiance-toward-camera\";\n geometricRayProjectionCalculated: true;\n sceneIntersectionCalculated: false;\n visibilityCalculated: false;\n providerExecuted: false;\n}\n\n/**\n * Builds an environment radiance request from points on the physical sensor,\n * using the existing focus-aware ideal projection and the analytic inverse\n * camera rotation.\n *\n * Native Y (+Y down) is inverted explicitly. The provider direction points\n * opposite the look ray at exposure start. Nothing intersects the scene,\n * computes radiance or renders.\n */\nexport function calculateSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n requireInputKeys(input, \"calculateSensorEnvironmentRadianceQuery\", [\"sourcePointNativeSensorMm\", \"focalLengthMm\", \"focus\", \"angularVelocityRadPerSec\", \"timeReference\", \"timeSecondsFromOpeningReference\", \"environmentDirectionConvention\", \"request\", \"lensFieldMapping\"]);\n if (input.lensFieldMapping !== undefined) {\n return calculateMappedSensorEnvironmentRadianceQuery(input, prepareEnvironmentLensFieldMapping(input.lensFieldMapping));\n }\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: sourcePointImagePlaneMm, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n const referenceLookDirectionUnitVector = { x: point.x/length, y: point.y/length, z: z/length };\n const ray = referenceLookDirectionUnitVector;\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -ray.x, y: -ray.y, z: -ray.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector, projection,\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"sensor-environment-radiance-query\", \"0.1.0\", [\n \"Ideal focus-aware rectilinear projection and constant-axis pure camera rotation; reference ray must stay in the forward hemisphere.\",\n \"The environment and rotation reference axes coincide with camera axes at the first opening boundary by explicit convention.\",\n \"Lens distortion, breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n\n/**\n * @internal The ideal (undistorted) image point of a physical sensor point (+X right, +Y up, mm) and the distortion area\n * factor 1 / det(d distorted / d ideal) there; the ideal point is the sensor point itself without distortion. Shared by\n * the mapped query below and the image-space defocus of `simulateCapture` (#548), so both use one arithmetic.\n */\nexport function undistortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, imagePointMm: { x: number; y: number }):\n { ideal: { x: number; y: number }; distortionAreaFactor: number } {\n let ideal = imagePointMm;\n let distortionAreaFactor = 1;\n if (prepared.radial !== undefined) {\n ideal = calculateInverseRadialSourcePointValue(imagePointMm, prepared.radial, \"sourcePointNativeSensorMm\").sourceImagePointMm;\n // Radial map p -> s(r) p: det = s (s + r ds/dr) with r ds/dr = 2 r^2 s'(r^2).\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n distortionAreaFactor = 1/(scale*radialDerivative);\n } else if (prepared.brownConrady !== undefined) {\n ideal = brownConradyInverseValue(prepared.brownConrady, imagePointMm);\n const j = brownConradyForwardValue(prepared.brownConrady, ideal).jacobian;\n distortionAreaFactor = 1/(j[0]*j[3]-j[1]*j[2]);\n }\n if (!(distortionAreaFactor > 0) || !Number.isFinite(distortionAreaFactor)) {\n throw new InvalidScientificInputError(\"Distortion area factor must remain finite and positive.\");\n }\n return { ideal, distortionAreaFactor };\n}\n\n/**\n * @internal The forward distortion of an ideal image point (+X right, +Y up, mm) to the physical sensor point, with the\n * singular values of d distorted / d ideal there (the largest and smallest local scale of the map; 1 and 1 without\n * distortion). The inverse of {@link undistortedImagePoint} up to its solver's rounding (#548).\n */\nexport function distortedImagePoint(prepared: PreparedEnvironmentLensFieldMapping, ideal: { x: number; y: number }):\n { pointMm: { x: number; y: number }; largestScale: number; smallestScale: number } {\n if (prepared.radial !== undefined) {\n const { k1, k2, k3 } = prepared.radial.coefficients;\n const r2 = (ideal.x*ideal.x+ideal.y*ideal.y)/prepared.radial.normalizationRadiusMm**2;\n const scale = 1+k1*r2+k2*r2*r2+k3*r2*r2*r2;\n // Tangential scale s, radial scale s + r ds/dr (the two eigenvalues of the symmetric Jacobian of p -> s(r) p).\n const radialDerivative = 1+3*k1*r2+5*k2*r2*r2+7*k3*r2*r2*r2;\n return { pointMm: { x: ideal.x*scale, y: ideal.y*scale }, largestScale: Math.max(scale, radialDerivative), smallestScale: Math.min(scale, radialDerivative) };\n }\n if (prepared.brownConrady !== undefined) {\n const forward = brownConradyForwardValue(prepared.brownConrady, ideal), [a, b, c, d] = forward.jacobian;\n // Singular values of [[a, b], [c, d]]: sqrt of the eigenvalues of J^T J.\n const p = a*a+b*b+c*c+d*d, q = Math.abs(a*d-b*c), root = Math.sqrt(Math.max(0, p*p-4*q*q));\n const largest = Math.sqrt((p+root)/2);\n return { pointMm: forward.mappedImagePointMm, largestScale: largest, smallestScale: largest > 0 ? q/largest : 0 };\n }\n return { pointMm: { ...ideal }, largestScale: 1, smallestScale: 1 };\n}\n\n/**\n * @internal Query with a lens field mapping that is already validated. The environment photo-signal\n * planner shares it, so profiles are validated once per plan.\n */\nexport function calculateMappedSensorEnvironmentRadianceQuery(\n input: CalculateSensorEnvironmentRadianceQueryInput,\n prepared: PreparedEnvironmentLensFieldMapping\n): CalculationResult<SensorEnvironmentRadianceQuery> {\n if (input.timeReference !== \"first-opening-boundary-phase\" ||\n input.environmentDirectionConvention !== \"outgoing-radiance-toward-camera\") {\n throw new InvalidScientificInputError(\"Environment projection requires explicit opening-boundary time and outgoing-toward-camera direction conventions.\");\n }\n const focus = parseFocusPlane(input.focus);\n const sourcePointImagePlaneMm = { x: input.sourcePointNativeSensorMm.x, y: -input.sourcePointNativeSensorMm.y };\n const mapping = prepared.mapping;\n const { ideal, distortionAreaFactor } = undistortedImagePoint(prepared, sourcePointImagePlaneMm);\n let projection: ReturnType<typeof calculateInverseCameraRotationImageMapping> | null = null;\n let look: { x: number; y: number; z: number };\n let projectionScaleMm: number;\n if (mapping.projectionModel === \"rectilinear\") {\n projection = calculateInverseCameraRotationImageMapping({ focalLengthMm: input.focalLengthMm,\n imagePointMm: ideal, angularVelocityRadPerSec: input.angularVelocityRadPerSec,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n ...(focus.kind === \"finite\" ? { focusDistanceM: focus.distanceM } : {}) });\n const point = projection.value.referenceImagePointMm, z = projection.value.projectionDistanceMm;\n const length = hypot3(point.x, point.y, z);\n look = { x: point.x/length, y: point.y/length, z: z/length };\n projectionScaleMm = z;\n } else {\n projectionScaleMm = focus.kind === \"finite\"\n ? calculateThinLensImageDistance({ focalLengthMm: input.focalLengthMm, objectDistanceM: focus.distanceM }).value.imageDistanceMm\n : input.focalLengthMm;\n const velocity = input.angularVelocityRadPerSec;\n const t = input.timeSecondsFromOpeningReference;\n if (typeof t !== \"number\" || !Number.isFinite(t) || t < 0) {\n throw new InvalidScientificInputError(\"timeSecondsFromOpeningReference must be finite and non-negative.\");\n }\n const captured = lensProjectionDirectionValue(mapping.projectionModel, projectionScaleMm, ideal.x, ideal.y);\n const reference = applyPhysicalCameraRotation(captured,\n { pitch: velocity.pitch*t, yaw: velocity.yaw*t, roll: velocity.roll*t }, \"camera-to-reference\");\n const length = hypot3(reference.x, reference.y, reference.z);\n if (!(length > 0) || !Number.isFinite(length)) throw new InvalidScientificInputError(\"Mapped reference ray must be finite.\");\n look = { x: reference.x/length, y: reference.y/length, z: reference.z/length };\n }\n const request = parseSceneRadianceEvaluationRequest({ ...input.request,\n timeSecondsFromExposureStart: input.timeSecondsFromOpeningReference,\n target: { kind: \"environment-direction\", outgoingDirectionUnitVector: { x: -look.x, y: -look.y, z: -look.z } } });\n return approximationResult({ request, sourcePointNativeSensorMm: { ...input.sourcePointNativeSensorMm },\n sourcePointImagePlaneMm, referenceLookDirectionUnitVector: look, projection,\n lensFieldMapping: { projectionModel: mapping.projectionModel, distortionKind: mapping.distortion.kind,\n idealImagePointMm: { ...ideal }, projectionScaleMm, distortionAreaFactor },\n targetMeaning: \"environment-direction-in-exposure-start-camera-axes\", directionConvention: \"outgoing-radiance-toward-camera\",\n geometricRayProjectionCalculated: true, sceneIntersectionCalculated: false, visibilityCalculated: false, providerExecuted: false\n }, \"lens-mapped-sensor-environment-radiance-query\", \"0.1.0\", [\n \"The physical sensor point is inverse-distorted to the ideal image point, then unprojected with the declared ideal projection and rotated by the constant-axis pure camera rotation.\",\n \"Projection scale is the thin-lens image distance at finite focus (focal length at infinity) for every projection model; a calibrated finite-focus fisheye scale is not modeled.\",\n \"The distortion area factor 1/det(d distorted/d ideal) is reported for exactly-once application to irradiance; field throughput, PSF and pupil models are not changed by this query.\",\n \"Breathing, chromatic field mapping, translation/parallax and scene intersections remain separate.\"\n ]);\n}\n"]}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { type SdrRenderingProfile } from "../output/sdr-rendering.js";
|
|
2
|
+
/** Version of the `development-detail` model (diffraction correction and unsharp mask). */
|
|
3
|
+
export declare const DEVELOPMENT_DETAIL_MODEL_VERSION: "0.1.0";
|
|
4
|
+
/** Unsharp mask settings: Gaussian standard deviation in output pixels, gain on the detail signal, and threshold in encoded units [0, 1]. */
|
|
5
|
+
export interface UnsharpMaskSettings {
|
|
6
|
+
radiusPixels: number;
|
|
7
|
+
amount: number;
|
|
8
|
+
threshold: number;
|
|
9
|
+
}
|
|
10
|
+
/** Diffraction correction settings: design wavelength, Wiener noise-to-signal regularization, gain bound and FIR radius in pixels. */
|
|
11
|
+
export interface DiffractionCorrectionSettings {
|
|
12
|
+
wavelengthNm: number;
|
|
13
|
+
regularization: number;
|
|
14
|
+
maximumGain: number;
|
|
15
|
+
kernelRadiusPixels: number;
|
|
16
|
+
}
|
|
17
|
+
/** Accepted ranges. Bounds are engineering limits on cost and on noise gain, not quality recommendations. */
|
|
18
|
+
export declare const DEVELOPMENT_DETAIL_LIMITS: Readonly<{
|
|
19
|
+
maximumUnsharpRadiusPixels: 8;
|
|
20
|
+
maximumUnsharpAmount: 5;
|
|
21
|
+
minimumWavelengthNm: 380;
|
|
22
|
+
maximumWavelengthNm: 780;
|
|
23
|
+
minimumRegularization: 0.0001;
|
|
24
|
+
maximumRegularization: 1;
|
|
25
|
+
maximumGain: 10;
|
|
26
|
+
maximumKernelRadiusPixels: 16;
|
|
27
|
+
}>;
|
|
28
|
+
/** Validates unsharp mask settings. */
|
|
29
|
+
export declare function parseUnsharpMaskSettings(value: UnsharpMaskSettings, path?: string): UnsharpMaskSettings;
|
|
30
|
+
/** Validates diffraction correction settings. */
|
|
31
|
+
export declare function parseDiffractionCorrectionSettings(value: DiffractionCorrectionSettings, path?: string): DiffractionCorrectionSettings;
|
|
32
|
+
/** Default FIR radius: the Airy pattern's third dark ring (3.2383·λ·N_w) in pixels, rounded up, within 1..16. */
|
|
33
|
+
export declare function defaultDiffractionCorrectionRadiusPixels(workingFNumber: number, wavelengthNm: number, pixelPitchMicrometers: number): number;
|
|
34
|
+
/**
|
|
35
|
+
* Incoherent OTF of an ideal, aberration-free circular pupil at normalized frequency s = f/f_c:
|
|
36
|
+
* H(s) = (2/π)(acos s − s·√(1 − s²)) for s < 1, and 0 for s ≥ 1 (MIT OCW 2.71, lectures 21–22; Goodman, §7.3).
|
|
37
|
+
*/
|
|
38
|
+
export declare function circularPupilDiffractionMtf(normalizedFrequency: number): number;
|
|
39
|
+
/** A designed diffraction correction filter and what its response does. Frequencies are in cycles per pixel. */
|
|
40
|
+
export interface DiffractionCorrectionFilter {
|
|
41
|
+
settings: DiffractionCorrectionSettings;
|
|
42
|
+
workingFNumber: number;
|
|
43
|
+
pixelPitchMicrometers: number;
|
|
44
|
+
/** f_c · pitch = pitch/(λ·N_w): the diffraction cut-off. The sampling Nyquist frequency is 0.5. */
|
|
45
|
+
cutoffCyclesPerPixel: number;
|
|
46
|
+
/** Fraction of the Nyquist square |fx|, |fy| ≤ 0.5 lying beyond the cut-off, where the lens transmitted nothing. */
|
|
47
|
+
nyquistAreaBeyondCutoffFraction: number;
|
|
48
|
+
/** Largest gain of the designed (untruncated) response: min(maximumGain, max over s of H(1+α)/(H² + α)). */
|
|
49
|
+
designPeakGain: number;
|
|
50
|
+
/** Largest |response| of the windowed, truncated FIR over a 1/64-cycle grid of the Nyquist square. */
|
|
51
|
+
responsePeakGain: number;
|
|
52
|
+
/**
|
|
53
|
+
* Largest |response| of the FIR at grid frequencies beyond the cut-off (null when the whole Nyquist square is inside it).
|
|
54
|
+
* The design is 0 there; a non-zero value is truncation leakage that passes noise and aliased signal, never lost detail,
|
|
55
|
+
* because the optics delivered no signal at those frequencies.
|
|
56
|
+
*/
|
|
57
|
+
responseMaximumBeyondCutoff: number | null;
|
|
58
|
+
/** Row-major (2R+1)² weights summing to 1; weights[(dy + R)(2R + 1) + dx + R]. */
|
|
59
|
+
weights: Float64Array;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Designs the bounded Wiener–Tikhonov correction. In frequency (normalized s = ρ/f_c, ρ the radial frequency):
|
|
63
|
+
* G(s) = min(maximumGain, H(s)·(1 + α)/(H(s)² + α)), G(0) = 1, and G(s) = 0 for s ≥ 1,
|
|
64
|
+
* where α is the regularization (an assumed noise-to-signal power ratio, Wiener's K). The response is sampled on an M × M
|
|
65
|
+
* grid (M a power of two ≥ max(64, 4(2R + 1))), inverse transformed, truncated to the (2R + 1)² window with a radial raised
|
|
66
|
+
* cosine taper and renormalized to unit sum. The achieved response is then measured, not assumed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function designDiffractionCorrectionFilter(input: {
|
|
69
|
+
workingFNumber: number;
|
|
70
|
+
pixelPitchMicrometers: number;
|
|
71
|
+
settings: DiffractionCorrectionSettings;
|
|
72
|
+
}): DiffractionCorrectionFilter;
|
|
73
|
+
/** Frequency response Σ w(x, y)·cos(2π(fx·x + fy·y)) of a centred (2R + 1)² FIR at (fx, fy) cycles per pixel. */
|
|
74
|
+
export declare function evaluateDiffractionCorrectionResponse(weights: Float64Array, radius: number, fx: number, fy: number): number;
|
|
75
|
+
/** What the correction did to the linear samples. */
|
|
76
|
+
export interface DiffractionCorrectionDiagnostics {
|
|
77
|
+
/** Corrected samples below 0 (ringing below black). They are clipped by the later gamut handling. */
|
|
78
|
+
negativeSampleCount: number;
|
|
79
|
+
/** Corrected samples above 1 (above the RAW white level). */
|
|
80
|
+
aboveWhiteSampleCount: number;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Applies the FIR to interleaved linear RGB (width × height × 3), each channel separately, edges clamped. Returns new samples.
|
|
84
|
+
*/
|
|
85
|
+
export declare function applyDiffractionCorrection(rgb: Float64Array, width: number, height: number, filter: Pick<DiffractionCorrectionFilter, "weights" | "settings">): {
|
|
86
|
+
samples: Float64Array;
|
|
87
|
+
diagnostics: DiffractionCorrectionDiagnostics;
|
|
88
|
+
};
|
|
89
|
+
/** Normalized Gaussian weights exp(−i²/(2σ²)), i = −r..r with r = ⌈3σ⌉. */
|
|
90
|
+
export declare function unsharpMaskKernel(radiusPixels: number): Float64Array;
|
|
91
|
+
/** What the unsharp mask did. Counts are samples (one per channel). */
|
|
92
|
+
export interface UnsharpMaskDiagnostics {
|
|
93
|
+
/** Half-width of the separable Gaussian, ⌈3σ⌉ pixels. */
|
|
94
|
+
kernelRadiusPixels: number;
|
|
95
|
+
/** Samples changed by the mask (|detail| ≥ threshold and detail ≠ 0). */
|
|
96
|
+
sharpenedSampleCount: number;
|
|
97
|
+
/** Samples left unchanged because 0 < |detail| < threshold. */
|
|
98
|
+
thresholdHeldSampleCount: number;
|
|
99
|
+
/** Sharpened samples clipped at 0 or 1 (the halo did not fit in the code range). */
|
|
100
|
+
clippedLowSampleCount: number;
|
|
101
|
+
clippedHighSampleCount: number;
|
|
102
|
+
/** Largest increase and decrease amount·detail before clipping, in encoded units: the halo strength. */
|
|
103
|
+
maximumIncrease: number;
|
|
104
|
+
maximumDecrease: number;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Unsharp mask on interleaved encoded RGB in [0, 1] (each channel separately, edges clamped):
|
|
108
|
+
* d = x − (G_σ ∗ x); y = x + amount·d where |d| ≥ threshold, else y = x; then y is clipped to [0, 1].
|
|
109
|
+
* Returns new samples.
|
|
110
|
+
*/
|
|
111
|
+
export declare function applyUnsharpMask(encoded: Float64Array, width: number, height: number, settings: UnsharpMaskSettings): {
|
|
112
|
+
samples: Float64Array;
|
|
113
|
+
diagnostics: UnsharpMaskDiagnostics;
|
|
114
|
+
};
|
|
115
|
+
/** SDR counters in the shape of the SDR rendering diagnostics. */
|
|
116
|
+
export interface DetailSdrCounts {
|
|
117
|
+
negative: number;
|
|
118
|
+
above: number;
|
|
119
|
+
changed: number;
|
|
120
|
+
low: number;
|
|
121
|
+
high: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* SDR rendering with an unsharp mask between the sRGB transfer and quantization. Per pixel it runs the reference renderer's
|
|
125
|
+
* operations in its order (−0 → +0, rendering exposure 2^EV with reference white 1, the shared tone operator, gamut
|
|
126
|
+
* handling, sRGB transfer); the whole encoded image is then sharpened and rounded to nearest (ties up).
|
|
127
|
+
*/
|
|
128
|
+
export declare function renderSdrWithUnsharpMask(rgb: Float64Array, width: number, height: number, profile: SdrRenderingProfile, settings: UnsharpMaskSettings): {
|
|
129
|
+
codes: Uint8Array | Uint16Array;
|
|
130
|
+
counts: DetailSdrCounts;
|
|
131
|
+
sharpening: UnsharpMaskDiagnostics;
|
|
132
|
+
};
|