@photivra/engine 2.20.0 → 2.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (275) hide show
  1. package/CHANGELOG.md +295 -1
  2. package/CITATION.cff +1 -1
  3. package/README.md +7 -6
  4. package/SECURITY.md +1 -1
  5. package/THIRD_PARTY.md +18 -0
  6. package/dist/capture/c2pa-embed.d.ts +116 -0
  7. package/dist/capture/c2pa-embed.js +493 -0
  8. package/dist/capture/c2pa-embed.js.map +1 -0
  9. package/dist/capture/dng-import.d.ts +73 -8
  10. package/dist/capture/dng-import.js +315 -80
  11. package/dist/capture/dng-import.js.map +1 -1
  12. package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
  13. package/dist/capture/dng-lens-correction-opcodes.js +2 -2
  14. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
  15. package/dist/capture/dng-opcodes.d.ts +96 -0
  16. package/dist/capture/dng-opcodes.js +391 -0
  17. package/dist/capture/dng-opcodes.js.map +1 -0
  18. package/dist/capture/dng-profile-maps.d.ts +99 -0
  19. package/dist/capture/dng-profile-maps.js +381 -0
  20. package/dist/capture/dng-profile-maps.js.map +1 -0
  21. package/dist/capture/dng-stage-image.d.ts +23 -0
  22. package/dist/capture/dng-stage-image.js +65 -0
  23. package/dist/capture/dng-stage-image.js.map +1 -0
  24. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  25. package/dist/capture/exposure-build-up-stage.js +4 -0
  26. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  27. package/dist/capture/jpeg-xl-tiles.d.ts +22 -0
  28. package/dist/capture/jpeg-xl-tiles.js +151 -0
  29. package/dist/capture/jpeg-xl-tiles.js.map +1 -0
  30. package/dist/capture/lossy-jpeg.d.ts +17 -0
  31. package/dist/capture/lossy-jpeg.js +369 -0
  32. package/dist/capture/lossy-jpeg.js.map +1 -0
  33. package/dist/capture/native-environment-lean.d.ts +2 -1
  34. package/dist/capture/native-environment-lean.js +41 -73
  35. package/dist/capture/native-environment-lean.js.map +1 -1
  36. package/dist/capture/native-environment-tabulated.js +4 -3
  37. package/dist/capture/native-environment-tabulated.js.map +1 -1
  38. package/dist/capture/native-raw-development.d.ts +2 -1
  39. package/dist/capture/native-raw-development.js.map +1 -1
  40. package/dist/capture/native-raw.d.ts +3 -2
  41. package/dist/capture/native-raw.js +5 -4
  42. package/dist/capture/native-raw.js.map +1 -1
  43. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  44. package/dist/capture/sensor-raw-producer.js +12 -3
  45. package/dist/capture/sensor-raw-producer.js.map +1 -1
  46. package/dist/color/neighbor-highlight-reconstruction.d.ts +66 -0
  47. package/dist/color/neighbor-highlight-reconstruction.js +234 -0
  48. package/dist/color/neighbor-highlight-reconstruction.js.map +1 -0
  49. package/dist/color/sensor-color-development.d.ts +5 -0
  50. package/dist/color/sensor-color-development.js +7 -1
  51. package/dist/color/sensor-color-development.js.map +1 -1
  52. package/dist/color/spectral-spikiness.d.ts +11 -0
  53. package/dist/color/spectral-spikiness.js +23 -0
  54. package/dist/color/spectral-spikiness.js.map +1 -0
  55. package/dist/core/execution-error.d.ts +21 -0
  56. package/dist/core/execution-error.js +12 -0
  57. package/dist/core/execution-error.js.map +1 -0
  58. package/dist/core/version.d.ts +1 -1
  59. package/dist/core/version.js +1 -1
  60. package/dist/core/version.js.map +1 -1
  61. package/dist/equipment/generic-lens-catalogue.d.ts +3 -2
  62. package/dist/equipment/generic-lens-catalogue.js +18 -5
  63. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  64. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  65. package/dist/equipment/generic-tier-assets.js +16 -11
  66. package/dist/equipment/generic-tier-assets.js.map +1 -1
  67. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  68. package/dist/equipment/sensor-profile-body.js +77 -0
  69. package/dist/equipment/sensor-profile-body.js.map +1 -0
  70. package/dist/equipment/sensor-profiles.d.ts +8 -0
  71. package/dist/equipment/sensor-profiles.js +82 -0
  72. package/dist/equipment/sensor-profiles.js.map +1 -0
  73. package/dist/facade/catalogue-lens-signal.d.ts +28 -3
  74. package/dist/facade/catalogue-lens-signal.js +83 -78
  75. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  76. package/dist/facade/chart-radiance-batch.js +2 -1
  77. package/dist/facade/chart-radiance-batch.js.map +1 -1
  78. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  79. package/dist/facade/defocus-image-space-provider.js +64 -76
  80. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  81. package/dist/facade/diffraction-provider.d.ts +1 -1
  82. package/dist/facade/diffraction-provider.js +4 -4
  83. package/dist/facade/diffraction-provider.js.map +1 -1
  84. package/dist/facade/display-view-gpu-session.js +39 -5
  85. package/dist/facade/display-view-gpu-session.js.map +1 -1
  86. package/dist/facade/display-view-gpu.d.ts +19 -6
  87. package/dist/facade/display-view-gpu.js.map +1 -1
  88. package/dist/facade/display-view-lens-signal.js +25 -23
  89. package/dist/facade/display-view-lens-signal.js.map +1 -1
  90. package/dist/facade/display-view-normal.d.ts +15 -0
  91. package/dist/facade/display-view-normal.js +44 -0
  92. package/dist/facade/display-view-normal.js.map +1 -0
  93. package/dist/facade/display-view-rendering.d.ts +97 -0
  94. package/dist/facade/display-view-rendering.js +598 -0
  95. package/dist/facade/display-view-rendering.js.map +1 -0
  96. package/dist/facade/display-view.d.ts +37 -65
  97. package/dist/facade/display-view.js +152 -499
  98. package/dist/facade/display-view.js.map +1 -1
  99. package/dist/facade/expected-signal-setup.d.ts +3 -1
  100. package/dist/facade/expected-signal-setup.js +3 -3
  101. package/dist/facade/expected-signal-setup.js.map +1 -1
  102. package/dist/facade/expected-signal.d.ts +47 -14
  103. package/dist/facade/expected-signal.js +117 -112
  104. package/dist/facade/expected-signal.js.map +1 -1
  105. package/dist/facade/exposure-build-up.d.ts +70 -0
  106. package/dist/facade/exposure-build-up.js +164 -0
  107. package/dist/facade/exposure-build-up.js.map +1 -0
  108. package/dist/facade/fast-methods.d.ts +1 -1
  109. package/dist/facade/fast-methods.js +46 -9
  110. package/dist/facade/fast-methods.js.map +1 -1
  111. package/dist/facade/generic-lens-capture.d.ts +3 -3
  112. package/dist/facade/generic-lens-capture.js +3 -3
  113. package/dist/facade/generic-lens-capture.js.map +1 -1
  114. package/dist/facade/image-plane-enable.d.ts +7 -0
  115. package/dist/facade/image-plane-enable.js +17 -0
  116. package/dist/facade/image-plane-enable.js.map +1 -0
  117. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  118. package/dist/facade/image-plane-lattice-data.js +10 -0
  119. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  120. package/dist/facade/image-plane-support.d.ts +33 -0
  121. package/dist/facade/image-plane-support.js +11 -0
  122. package/dist/facade/image-plane-support.js.map +1 -0
  123. package/dist/facade/image-plane.d.ts +36 -0
  124. package/dist/facade/image-plane.js +384 -0
  125. package/dist/facade/image-plane.js.map +1 -0
  126. package/dist/facade/mixed-illumination.d.ts +108 -0
  127. package/dist/facade/mixed-illumination.js +174 -0
  128. package/dist/facade/mixed-illumination.js.map +1 -0
  129. package/dist/facade/on-demand-kits.d.ts +14 -0
  130. package/dist/facade/on-demand-kits.js +36 -0
  131. package/dist/facade/on-demand-kits.js.map +1 -0
  132. package/dist/facade/opt-in-kit.d.ts +9 -0
  133. package/dist/facade/opt-in-kit.js +11 -0
  134. package/dist/facade/opt-in-kit.js.map +1 -0
  135. package/dist/facade/quality-kit.d.ts +21 -0
  136. package/dist/facade/quality-kit.js +23 -0
  137. package/dist/facade/quality-kit.js.map +1 -0
  138. package/dist/facade/quality-paths.d.ts +22 -0
  139. package/dist/facade/quality-paths.js +44 -0
  140. package/dist/facade/quality-paths.js.map +1 -0
  141. package/dist/facade/representative-light-sources.d.ts +72 -0
  142. package/dist/facade/representative-light-sources.js +124 -0
  143. package/dist/facade/representative-light-sources.js.map +1 -0
  144. package/dist/facade/scene-radiance.d.ts +145 -4
  145. package/dist/facade/scene-radiance.js +50 -13
  146. package/dist/facade/scene-radiance.js.map +1 -1
  147. package/dist/facade/simulate-capture.d.ts +68 -20
  148. package/dist/facade/simulate-capture.js +399 -146
  149. package/dist/facade/simulate-capture.js.map +1 -1
  150. package/dist/facade/simulate-display-view.d.ts +10 -12
  151. package/dist/facade/simulate-display-view.js +70 -53
  152. package/dist/facade/simulate-display-view.js.map +1 -1
  153. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  154. package/dist/gpu/chart-composite-gpu.js +48 -9
  155. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  156. package/dist/gpu/chart-radiance-gpu.js +4 -30
  157. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  158. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  159. package/dist/gpu/display-view-composite-material-module.js +39 -13
  160. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  161. package/dist/gpu/display-view-gpu-renderer.d.ts +43 -0
  162. package/dist/gpu/display-view-gpu-renderer.js +134 -0
  163. package/dist/gpu/display-view-gpu-renderer.js.map +1 -0
  164. package/dist/gpu/display-view-image-material-module.d.ts +87 -0
  165. package/dist/gpu/display-view-image-material-module.js +361 -0
  166. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  167. package/dist/gpu/display-view-image-quality.d.ts +87 -0
  168. package/dist/gpu/display-view-image-quality.js +625 -0
  169. package/dist/gpu/display-view-image-quality.js.map +1 -0
  170. package/dist/gpu/display-view-quality-gpu.d.ts +80 -65
  171. package/dist/gpu/display-view-quality-gpu.js +187 -445
  172. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  173. package/dist/gpu/display-view-speed-gpu.d.ts +67 -19
  174. package/dist/gpu/display-view-speed-gpu.js +181 -105
  175. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  176. package/dist/gpu/image-kernel-accelerator.js +3 -0
  177. package/dist/gpu/image-kernel-accelerator.js.map +1 -1
  178. package/dist/gpu/image-kernel-execution.d.ts +2 -13
  179. package/dist/gpu/image-kernel-execution.js +2 -10
  180. package/dist/gpu/image-kernel-execution.js.map +1 -1
  181. package/dist/gpu/wgsl-display-view-coupled-stub.d.ts +8 -0
  182. package/dist/gpu/wgsl-display-view-coupled-stub.js +21 -0
  183. package/dist/gpu/wgsl-display-view-coupled-stub.js.map +1 -0
  184. package/dist/gpu/wgsl-display-view-coupled.d.ts +88 -0
  185. package/dist/gpu/wgsl-display-view-coupled.js +353 -0
  186. package/dist/gpu/wgsl-display-view-coupled.js.map +1 -0
  187. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +33 -24
  188. package/dist/gpu/wgsl-display-view-quality-kernel.js +166 -146
  189. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  190. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +19 -44
  191. package/dist/gpu/wgsl-display-view-speed-kernel.js +116 -18
  192. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  193. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  194. package/dist/gpu/wgsl-exact-sum.js +42 -0
  195. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  196. package/dist/index.d.ts +22 -6
  197. package/dist/index.js +16 -2
  198. package/dist/index.js.map +1 -1
  199. package/dist/motion/subject-motion.d.ts +66 -0
  200. package/dist/motion/subject-motion.js +52 -0
  201. package/dist/motion/subject-motion.js.map +1 -0
  202. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  203. package/dist/optics/generic-lens-lateral-color.js +77 -0
  204. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  205. package/dist/optics/generic-lens-psf.js +54 -46
  206. package/dist/optics/generic-lens-psf.js.map +1 -1
  207. package/dist/optics/lens-psf-profile.d.ts +7 -0
  208. package/dist/optics/lens-psf-profile.js +2 -1
  209. package/dist/optics/lens-psf-profile.js.map +1 -1
  210. package/dist/output/camera-channel-response.d.ts +50 -0
  211. package/dist/output/camera-channel-response.js +69 -0
  212. package/dist/output/camera-channel-response.js.map +1 -0
  213. package/dist/output/developed-lens-corrections.d.ts +19 -5
  214. package/dist/output/developed-lens-corrections.js +28 -9
  215. package/dist/output/developed-lens-corrections.js.map +1 -1
  216. package/dist/output/generic-lens-corrections.d.ts +41 -5
  217. package/dist/output/generic-lens-corrections.js +52 -8
  218. package/dist/output/generic-lens-corrections.js.map +1 -1
  219. package/dist/output/native-capture-sdr.js +5 -2
  220. package/dist/output/native-capture-sdr.js.map +1 -1
  221. package/dist/output/print-plan.d.ts +41 -5
  222. package/dist/output/print-plan.js +59 -14
  223. package/dist/output/print-plan.js.map +1 -1
  224. package/dist/output/sdr-rendering.d.ts +18 -1
  225. package/dist/output/sdr-rendering.js +39 -2
  226. package/dist/output/sdr-rendering.js.map +1 -1
  227. package/dist/sensor/binomial-sample.d.ts +3 -0
  228. package/dist/sensor/binomial-sample.js +86 -0
  229. package/dist/sensor/binomial-sample.js.map +1 -0
  230. package/dist/sensor/native-sensor-report.d.ts +10 -0
  231. package/dist/sensor/native-sensor-report.js +16 -0
  232. package/dist/sensor/native-sensor-report.js.map +1 -0
  233. package/dist/sensor/sensor-characteristics.d.ts +91 -13
  234. package/dist/sensor/sensor-characteristics.js +30 -15
  235. package/dist/sensor/sensor-characteristics.js.map +1 -1
  236. package/docs/AGENT_GUIDE.md +1 -0
  237. package/docs/API_REFERENCE.md +214 -4
  238. package/docs/QUICK_START.md +162 -8
  239. package/docs/USAGE.md +195 -22
  240. package/docs/api/capture-c2pa-embed.md +177 -0
  241. package/docs/api/capture-dng-import.md +86 -10
  242. package/docs/api/capture-dng-opcodes.md +93 -0
  243. package/docs/api/capture-dng-profile-maps.md +141 -0
  244. package/docs/api/capture-dng-stage-image.md +43 -0
  245. package/docs/api/capture-jpeg-xl-tiles.md +33 -0
  246. package/docs/api/capture-lossy-jpeg.md +41 -0
  247. package/docs/api/capture-native-raw-development.md +2 -1
  248. package/docs/api/core-execution-error.md +43 -0
  249. package/docs/api/core-version.md +1 -1
  250. package/docs/api/equipment-generic-lens-catalogue.md +10 -5
  251. package/docs/api/equipment-sensor-profile-body.md +64 -0
  252. package/docs/api/equipment-sensor-profiles.md +86 -0
  253. package/docs/api/exports.json +460 -5
  254. package/docs/api/facade-display-view-gpu.md +5 -2
  255. package/docs/api/facade-display-view.md +11 -15
  256. package/docs/api/facade-exposure-build-up.md +105 -0
  257. package/docs/api/facade-fast-methods.md +47 -10
  258. package/docs/api/facade-image-plane-enable.md +16 -0
  259. package/docs/api/facade-mixed-illumination.md +67 -0
  260. package/docs/api/facade-quality-paths.md +24 -0
  261. package/docs/api/facade-representative-light-sources.md +131 -0
  262. package/docs/api/facade-scene-radiance.md +54 -3
  263. package/docs/api/facade-simulate-capture.md +48 -11
  264. package/docs/api/facade-simulate-display-view.md +10 -12
  265. package/docs/api/gpu-image-kernel-execution.md +0 -36
  266. package/docs/api/motion-subject-motion.md +23 -0
  267. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  268. package/docs/api/output-camera-channel-response.md +55 -0
  269. package/docs/api/output-developed-lens-corrections.md +10 -1
  270. package/docs/api/output-generic-lens-corrections.md +40 -4
  271. package/docs/api/output-print-plan.md +35 -1
  272. package/docs/api/output-sdr-rendering.md +6 -1
  273. package/docs/api/sensor-sensor-characteristics.md +54 -2
  274. package/llms.txt +3 -0
  275. package/package.json +6 -5
@@ -1,11 +1,23 @@
1
1
  import type { LensCorrectionChannel } from "./lens-corrections.js";
2
2
  /**
3
3
  * Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where
4
- * a camera's channel responses peak, not a measured camera.
4
+ * a camera's channel responses peak, not a measured camera. The default of `createGenericLensCorrectionProfile` without a
5
+ * `channelDispersion`; `simulateCapture`'s `output.lensCorrections` uses the capture's own channels instead (#589).
5
6
  */
6
7
  export declare const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM: Readonly<Record<LensCorrectionChannel, number>>;
7
- /** @internal The lateral-colour scale of one channel, 1 + L·D(λ_channel) (`D` the catalogue's normalized dispersion). */
8
- export declare function genericLensChannelScale(fraction: number, dispersion: (wavelengthNm: number) => number, channel: LensCorrectionChannel): number;
8
+ /** A normalized dispersion per channel (`genericLensDispersion` units: 0 at the d line, F minus C = 1). */
9
+ export type ChannelDispersion = Readonly<Record<LensCorrectionChannel, number>>;
10
+ /**
11
+ * @internal #589: each channel's effective dispersion D̄_c = Σ_n w_c,n·D(λ_n) / Σ_n w_c,n, w_c,n = response_c(λ_n)·illuminant(λ_n), the
12
+ * dispersion a neutral surface's image in that channel carries (its centroid moves by L·r·D̄_c under lateral colour L·r·D(λ)).
13
+ * `response` holds the red, green and blue values at the nodes `wavelengthsNm`; each channel needs a positive total weight.
14
+ */
15
+ export declare function channelEffectiveDispersion(wavelengthsNm: readonly number[], response: readonly ArrayLike<number>[], illuminant: ArrayLike<number>): ChannelDispersion;
16
+ /**
17
+ * @internal The lateral-colour scale of one channel, 1 + L·D̄ with D̄ the channel's effective dispersion, or D(λ_channel) at
18
+ * {@link GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM} without one (`D` the catalogue's normalized dispersion).
19
+ */
20
+ export declare function genericLensChannelScale(fraction: number, channel: LensCorrectionChannel, channels?: ChannelDispersion): number;
9
21
  /** @internal Which corrections run. */
10
22
  export interface DevelopedLensCorrectionSelection {
11
23
  distortion: boolean;
@@ -33,7 +45,9 @@ type Rect = {
33
45
  };
34
46
  /**
35
47
  * @internal Prepares the correction of a W × H frame. `lens` is the resolved catalogue lens (`resolveGenericLens`) on the
36
- * frame; `dispersion` is `genericLensDispersion`. The mapping's source radius grows with the output radius (the catalogue's
48
+ * frame; `channels` the channels' effective dispersion (#589). Without it the scales span every channel's possible dispersion
49
+ * (red and green at D(780 nm), blue at D(380 nm), the spectral range's ends): `simulateCapture` prepares that first, so the
50
+ * developed rectangle a region reads (`developedLensCorrectionSourceRect`) holds the sources of any channel scales. The mapping's source radius grows with the output radius (the catalogue's
37
51
  * distortion is monotonic over the frame), so the sources of the border pixel centres bound every source, and the fill factor
38
52
  * is found by bisection on them.
39
53
  */
@@ -47,7 +61,7 @@ export declare function prepareDevelopedLensCorrection(lens: {
47
61
  fraction: number;
48
62
  };
49
63
  };
50
- }, selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, dispersion: (wavelengthNm: number) => number): DevelopedLensCorrection;
64
+ }, selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, channels?: ChannelDispersion): DevelopedLensCorrection;
51
65
  /**
52
66
  * @internal The developed pixels (a rectangle of the whole frame) that the corrected pixels of `region` read: the bounding box of
53
67
  * the region border's sources over the three channels, with their bilinear neighbours, one pixel of margin, clipped to the frame.
@@ -19,14 +19,32 @@
19
19
  * Every position is computed in whole-frame pixel coordinates, so a region renders the same values as the whole frame.
20
20
  */
21
21
  import { InvalidScientificResultError } from "../core/validation.js";
22
+ import { genericLensDispersion } from "../equipment/generic-lens-catalogue.js";
22
23
  /**
23
24
  * Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where
24
- * a camera's channel responses peak, not a measured camera.
25
+ * a camera's channel responses peak, not a measured camera. The default of `createGenericLensCorrectionProfile` without a
26
+ * `channelDispersion`; `simulateCapture`'s `output.lensCorrections` uses the capture's own channels instead (#589).
25
27
  */
26
28
  export const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM = /* @__PURE__ */ Object.freeze({ red: 610, green: 540, blue: 465 });
27
- /** @internal The lateral-colour scale of one channel, 1 + L·D(λ_channel) (`D` the catalogue's normalized dispersion). */
28
- export function genericLensChannelScale(fraction, dispersion, channel) {
29
- return 1 + fraction * dispersion(GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM[channel]);
29
+ /**
30
+ * @internal #589: each channel's effective dispersion D̄_c = Σ_n w_c,n·D(λ_n) / Σ_n w_c,n, w_c,n = response_c(λ_n)·illuminant(λ_n), the
31
+ * dispersion a neutral surface's image in that channel carries (its centroid moves by L·r·D̄_c under lateral colour L·r·D(λ)).
32
+ * `response` holds the red, green and blue values at the nodes `wavelengthsNm`; each channel needs a positive total weight.
33
+ */
34
+ export function channelEffectiveDispersion(wavelengthsNm, response, illuminant) {
35
+ const [red, green, blue] = response.map((r) => {
36
+ let weighted = 0, total = 0;
37
+ wavelengthsNm.forEach((nm, n) => { const w = r[n] * illuminant[n]; weighted += w * genericLensDispersion(nm); total += w; });
38
+ return weighted / total;
39
+ });
40
+ return { red, green, blue };
41
+ }
42
+ /**
43
+ * @internal The lateral-colour scale of one channel, 1 + L·D̄ with D̄ the channel's effective dispersion, or D(λ_channel) at
44
+ * {@link GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM} without one (`D` the catalogue's normalized dispersion).
45
+ */
46
+ export function genericLensChannelScale(fraction, channel, channels) {
47
+ return 1 + fraction * (channels ? channels[channel] : genericLensDispersion(GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM[channel]));
30
48
  }
31
49
  /** Source position (pixel-centre coordinates of the whole frame, unclamped) of output pixel (x, y), channel c. */
32
50
  function sourceOf(c, m, x, y, channel, out) {
@@ -48,15 +66,16 @@ function border(r, visit) {
48
66
  }
49
67
  /**
50
68
  * @internal Prepares the correction of a W × H frame. `lens` is the resolved catalogue lens (`resolveGenericLens`) on the
51
- * frame; `dispersion` is `genericLensDispersion`. The mapping's source radius grows with the output radius (the catalogue's
69
+ * frame; `channels` the channels' effective dispersion (#589). Without it the scales span every channel's possible dispersion
70
+ * (red and green at D(780 nm), blue at D(380 nm), the spectral range's ends): `simulateCapture` prepares that first, so the
71
+ * developed rectangle a region reads (`developedLensCorrectionSourceRect`) holds the sources of any channel scales. The mapping's source radius grows with the output radius (the catalogue's
52
72
  * distortion is monotonic over the frame), so the sources of the border pixel centres bound every source, and the fill factor
53
73
  * is found by bisection on them.
54
74
  */
55
- export function prepareDevelopedLensCorrection(lens, selection, width, height, pitchMm, dispersion) {
56
- const fraction = selection.lateralColor ? lens.character.lateralColor.fraction : 0;
75
+ export function prepareDevelopedLensCorrection(lens, selection, width, height, pitchMm, channels = { red: genericLensDispersion(780), green: genericLensDispersion(780), blue: genericLensDispersion(380) }) {
76
+ const fraction = selection.lateralColor ? lens.character.lateralColor.fraction : 0, scale = (c) => genericLensChannelScale(fraction, c, channels);
57
77
  const c = { width, height, radius: lens.halfDiagonalMm / pitchMm,
58
- k1: selection.distortion ? lens.character.distortion.corner : 0, lensK1: lens.character.distortion.corner,
59
- scales: [genericLensChannelScale(fraction, dispersion, "red"), genericLensChannelScale(fraction, dispersion, "green"), genericLensChannelScale(fraction, dispersion, "blue")], fill: 1 };
78
+ k1: selection.distortion ? lens.character.distortion.corner : 0, lensK1: lens.character.distortion.corner, scales: [scale("red"), scale("green"), scale("blue")], fill: 1 };
60
79
  const p = new Float64Array(2), frame = { x: 0, y: 0, width, height };
61
80
  const fits = (m) => {
62
81
  let ok = true;
@@ -1 +1 @@
1
- {"version":3,"file":"developed-lens-corrections.js","sourceRoot":"","sources":["../../src/output/developed-lens-corrections.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC;;;;;;;;;;;;;;;;;;GAkBG;AAEH,OAAO,EAAE,4BAA4B,EAAE,MAAM,uBAAuB,CAAC;AAGrE;;;GAGG;AACH,MAAM,CAAC,MAAM,8CAA8C,GAAoD,eAAe,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,CAAC;AAElL,yHAAyH;AACzH,MAAM,UAAU,uBAAuB,CAAC,QAAgB,EAAE,UAA4C,EAAE,OAA8B;IACpI,OAAO,CAAC,GAAG,QAAQ,GAAG,UAAU,CAAC,8CAA8C,CAAC,OAAO,CAAC,CAAC,CAAC;AAC5F,CAAC;AAgBD,kHAAkH;AAClH,SAAS,QAAQ,CAAC,CAA0B,EAAE,CAAS,EAAE,CAAS,EAAE,CAAS,EAAE,OAAe,EAAE,GAAiB;IAC/G,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,MAAM,CAAC,OAAO,CAAE,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC;IACtG,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC;IACjE,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,GAAG,GAAG,CAAC;IAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;AAC5E,CAAC;AACD,iEAAiE;AACjE,SAAS,MAAM,CAAC,CAAO,EAAE,KAAqC;IAC5D,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC;QAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAAC,CAAC;IAC1F,KAAK,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAAC,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QAAC,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,CAAC,CAAC,CAAC;IAAC,CAAC;AAC5F,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,8BAA8B,CAAC,IAAmH,EAChK,SAA2C,EAAE,KAAa,EAAE,MAAc,EAAE,OAAe,EAAE,UAA4C;IACzI,MAAM,QAAQ,GAAG,SAAS,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;IACnF,MAAM,CAAC,GAA4B,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,CAAC,cAAc,GAAG,OAAO;QACvF,EAAE,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,MAAM;QACzG,MAAM,EAAE,CAAC,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,KAAK,CAAC,EAAE,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,OAAO,CAAC,EAAE,uBAAuB,CAAC,QAAQ,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC;IAC3L,MAAM,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,EAAE,KAAK,GAAG,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IACrE,MAAM,IAAI,GAAG,CAAC,CAAS,EAAW,EAAE;QAClC,IAAI,EAAE,GAAG,IAAI,CAAC;QACd,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;YACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;gBAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;gBAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAI,KAAK,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAE,IAAI,MAAM,GAAG,CAAC,CAAC;YAAC,CAAC;QAC/I,CAAC,CAAC,CAAC;QACH,OAAO,EAAE,CAAC;IACZ,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;QACb,IAAI,EAAE,GAAG,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC;QACrB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;YAAC,MAAM,GAAG,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;YAAC,IAAI,IAAI,CAAC,GAAG,CAAC;gBAAE,EAAE,GAAG,GAAG,CAAC;;gBAAM,EAAE,GAAG,GAAG,CAAC;QAAC,CAAC;QACnG,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC;IACd,CAAC;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iCAAiC,CAAC,CAA0B,EAAE,MAAY;IACxF,MAAM,CAAC,GAAG,IAAI,YAAY,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,EAAE,GAAG,QAAQ,EAAE,EAAE,GAAG,QAAQ,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,EAAE,GAAG,CAAC,QAAQ,CAAC;IACjE,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACtB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC;YAC3B,QAAQ,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;YAChC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;YAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;YAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;YAAC,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,CAAC,CAAC;QACzG,CAAC;IACH,CAAC,CAAC,CAAC;IACH,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACpF,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,GAAG,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,EAAE,CAAC;AACxJ,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,CAA0B,EAAE,GAAiB,EAAE,MAAY,EAAE,MAAY,EAAE,WAA0B;IAChJ,MAAM,GAAG,GAAG,IAAI,YAAY,CAAC,MAAM,CAAC,KAAK,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,EAAE,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IACxJ,MAAM,CAAC,GAAG,WAAW,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,WAAW,GAAG,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,EAAE,EAAE,MAAM,GAAG,CAAC,CAAC,MAAM,CAAC;IAC/F,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,CAAC,EAAE;YAAE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,CAAC;gBACtH,sHAAsH;gBACtH,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC,CAAC,CAAE,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC;gBACvJ,MAAM,EAAE,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,GAAG,GAAG,GAAG,EAAE,GAAG,CAAC,CAAC;gBAC3D,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;gBACjF,MAAM,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;gBACzH,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,MAAM,CAAC,CAAC,IAAI,EAAE,IAAI,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,IAAI,EAAE,IAAI,MAAM,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM;oBACjG,MAAM,IAAI,4BAA4B,CAAC,uDAAuD,CAAC,CAAC;gBAClG,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,KAAK,EAAE,EAAE,GAAG,CAAC,GAAG,MAAM,CAAC,CAAC,EAAE,EAAE,GAAG,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC;gBACxH,IAAI,KAAK,GAAG,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,GAAG,EAAE,GAAG,GAAG,CAAC,CAAC,IAAI,GAAG,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAE,CAAC,CAAC;gBAC5K,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;oBACZ,6HAA6H;oBAC7H,MAAM,EAAE,GAAG,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,CAAC,GAAG,EAAE,EAAE,EAAE,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC;oBACpG,IAAI,EAAE,GAAG,EAAE,CAAC;oBACZ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE;wBAAE,EAAE,IAAI,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,CAAC,CAAC;oBAClG,KAAK,IAAI,CAAC,CAAC,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;gBACzE,CAAC;gBACD,GAAG,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;YACjB,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * @internal `output.lensCorrections` (#570): a catalogue lens's generic correction profile (`createGenericLensCorrectionProfile`)\n * applied to the developed linear RGB of `simulateCapture`, after demosaic, colour and white balance and before the tone curve.\n * The RAW and the DNG are never corrected.\n *\n * For an output pixel at p (pixels from the frame centre, +y up) and channel c, the source point is read by inverse mapping,\n * as the profile composes it (lateral colour, then distortion):\n * q = m · a_c · p, s = q · (1 + k1·|q|²/R²),\n * with a_c = 1 + L·D(λ_c) the channel's lateral-colour scale, k1 the lens's corner distortion, R the half-diagonal and m ≤ 1 the\n * fill factor (the largest that keeps every output pixel centre's source inside the frame for every channel, so the output keeps\n * its size with no missing pixels; the image is magnified by 1/m). The channel is sampled bilinearly at s.\n *\n * Vignetting is corrected to a flat field: the sample is multiplied by det J(ρ_i) / E(ρ_s), the inverse of the relative\n * illumination the capture delivered at s: the lens's E(ρ) = (1 − bρ²)² at the sensor point and its distortion's area factor\n * 1/det J, det J = (1 + k1ρ_i²)(1 + 3k1ρ_i²) at the ideal point ρ_i that the lens images to ρ_s (the capture's\n * \"inverse-distortion-jacobian\"; a relative illumination measured on a flat field contains it too). From profile 1.1.0 (#584) the\n * profile's vignetting component carries the same area factor (`distortionAreaFactor`), and its gain equals this kernel's.\n * Every position is computed in whole-frame pixel coordinates, so a region renders the same values as the whole frame.\n */\n\nimport { InvalidScientificResultError } from \"../core/validation.js\";\nimport type { LensCorrectionChannel } from \"./lens-corrections.js\";\n\n/**\n * Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where\n * a camera's channel responses peak, not a measured camera.\n */\nexport const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM: Readonly<Record<LensCorrectionChannel, number>> = /* @__PURE__ */ Object.freeze({ red: 610, green: 540, blue: 465 });\n\n/** @internal The lateral-colour scale of one channel, 1 + L·D(λ_channel) (`D` the catalogue's normalized dispersion). */\nexport function genericLensChannelScale(fraction: number, dispersion: (wavelengthNm: number) => number, channel: LensCorrectionChannel): number {\n return 1 + fraction * dispersion(GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM[channel]);\n}\n\n/** @internal Which corrections run. */\nexport interface DevelopedLensCorrectionSelection { distortion: boolean; lateralColor: boolean; vignetting: boolean }\n/** @internal A prepared correction for one frame (pixel units). */\nexport interface DevelopedLensCorrection {\n width: number; height: number;\n /** Half-diagonal R in pixels. */\n radius: number;\n /** k1 (0 when distortion is off), the lens's own k1, and the red, green and blue scales a_c (1 when lateral colour is off). */\n k1: number; lensK1: number; scales: readonly [number, number, number];\n /** Fill factor m ≤ 1 (1/m is the magnification). */\n fill: number;\n}\ntype Rect = { x: number; y: number; width: number; height: number };\n\n/** Source position (pixel-centre coordinates of the whole frame, unclamped) of output pixel (x, y), channel c. */\nfunction sourceOf(c: DevelopedLensCorrection, m: number, x: number, y: number, channel: number, out: Float64Array): void {\n const a = m * c.scales[channel]!, px = (x + 0.5 - c.width / 2) * a, py = (c.height / 2 - y - 0.5) * a;\n const s = 1 + c.k1 * (px * px + py * py) / (c.radius * c.radius);\n out[0] = px * s + c.width / 2 - 0.5; out[1] = c.height / 2 - 0.5 - py * s;\n}\n/** Calls `visit` for every pixel centre on the border of `r`. */\nfunction border(r: Rect, visit: (x: number, y: number) => void): void {\n for (let x = r.x; x < r.x + r.width; x++) { visit(x, r.y); visit(x, r.y + r.height - 1); }\n for (let y = r.y; y < r.y + r.height; y++) { visit(r.x, y); visit(r.x + r.width - 1, y); }\n}\n\n/**\n * @internal Prepares the correction of a W × H frame. `lens` is the resolved catalogue lens (`resolveGenericLens`) on the\n * frame; `dispersion` is `genericLensDispersion`. The mapping's source radius grows with the output radius (the catalogue's\n * distortion is monotonic over the frame), so the sources of the border pixel centres bound every source, and the fill factor\n * is found by bisection on them.\n */\nexport function prepareDevelopedLensCorrection(lens: { halfDiagonalMm: number; character: { distortion: { corner: number }; lateralColor: { fraction: number } } },\n selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number, dispersion: (wavelengthNm: number) => number): DevelopedLensCorrection {\n const fraction = selection.lateralColor ? lens.character.lateralColor.fraction : 0;\n const c: DevelopedLensCorrection = { width, height, radius: lens.halfDiagonalMm / pitchMm,\n k1: selection.distortion ? lens.character.distortion.corner : 0, lensK1: lens.character.distortion.corner,\n scales: [genericLensChannelScale(fraction, dispersion, \"red\"), genericLensChannelScale(fraction, dispersion, \"green\"), genericLensChannelScale(fraction, dispersion, \"blue\")], fill: 1 };\n const p = new Float64Array(2), frame = { x: 0, y: 0, width, height };\n const fits = (m: number): boolean => {\n let ok = true;\n border(frame, (x, y) => {\n for (let k = 0; k < 3 && ok; k++) { sourceOf(c, m, x, y, k, p); ok = p[0]! >= 0 && p[0]! <= width - 1 && p[1]! >= 0 && p[1]! <= height - 1; }\n });\n return ok;\n };\n if (!fits(1)) {\n let lo = 0.5, hi = 1;\n for (let i = 0; i < 52; i++) { const mid = (lo + hi) / 2; if (fits(mid)) lo = mid; else hi = mid; }\n c.fill = lo;\n }\n return c;\n}\n\n/**\n * @internal The developed pixels (a rectangle of the whole frame) that the corrected pixels of `region` read: the bounding box of\n * the region border's sources over the three channels, with their bilinear neighbours, one pixel of margin, clipped to the frame.\n */\nexport function developedLensCorrectionSourceRect(c: DevelopedLensCorrection, region: Rect): Rect {\n const p = new Float64Array(2);\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n border(region, (x, y) => {\n for (let k = 0; k < 3; k++) {\n sourceOf(c, c.fill, x, y, k, p);\n x0 = Math.min(x0, p[0]!); x1 = Math.max(x1, p[0]!); y0 = Math.min(y0, p[1]!); y1 = Math.max(y1, p[1]!);\n }\n });\n const left = Math.max(0, Math.floor(x0) - 1), top = Math.max(0, Math.floor(y0) - 1);\n return { x: left, y: top, width: Math.min(c.width - 1, Math.floor(x1) + 2) - left + 1, height: Math.min(c.height - 1, Math.floor(y1) + 2) - top + 1 };\n}\n\n/**\n * @internal Corrects developed linear RGB (`rgb`, interleaved, the frame's pixels in `source`) into the pixels of `region`, both\n * rectangles of the whole frame. `cornerStops` is the lens's vignetting at the capture's f-number, or null to leave it.\n * Positions outside the frame (none after the fill, up to rounding) are clamped to the edge pixel centres.\n */\nexport function applyDevelopedLensCorrection(c: DevelopedLensCorrection, rgb: Float64Array, source: Rect, region: Rect, cornerStops: number | null): Float64Array {\n const out = new Float64Array(region.width * region.height * 3), W = c.width, H = c.height, cx = W / 2 - 0.5, cy = H / 2 - 0.5, r2 = c.radius * c.radius;\n const b = cornerStops === null ? 0 : 1 - 2 ** (-cornerStops / 2), k1 = c.k1, lensK1 = c.lensK1;\n for (let y = 0, o = 0; y < region.height; y++) for (let x = 0; x < region.width; x++) for (let k = 0; k < 3; k++, o++) {\n // sourceOf, inlined (a call per sample is about 5 times slower here): the same arithmetic, so the region bounds hold.\n const a = c.fill * c.scales[k]!, px = (region.x + x + 0.5 - W / 2) * a, py = (H / 2 - (region.y + y) - 0.5) * a, f = 1 + k1 * (px * px + py * py) / r2;\n const su = px * f + W / 2 - 0.5, sv = H / 2 - 0.5 - py * f;\n const u = Math.min(W - 1, Math.max(0, su)), v = Math.min(H - 1, Math.max(0, sv));\n const i = Math.floor(u), j = Math.floor(v), dx = u - i, dy = v - j, i1 = dx === 0 ? i : i + 1, j1 = dy === 0 ? j : j + 1;\n if (i < source.x || j < source.y || i1 >= source.x + source.width || j1 >= source.y + source.height)\n throw new InvalidScientificResultError(\"Lens correction read outside the developed rectangle.\");\n const row = (j - source.y) * source.width, row1 = (j1 - source.y) * source.width, a0 = i - source.x, a1 = i1 - source.x;\n let value = (1 - dy) * ((1 - dx) * rgb[(row + a0) * 3 + k]! + dx * rgb[(row + a1) * 3 + k]!) + dy * ((1 - dx) * rgb[(row1 + a0) * 3 + k]! + dx * rgb[(row1 + a1) * 3 + k]!);\n if (b !== 0) {\n // ρ_s², then ρ_i from ρ_s = ρ_i(1 + k1ρ_i²) by Newton's method (monotonic over the frame; converged to rounding in 4 steps).\n const s2 = ((su - cx) * (su - cx) + (sv - cy) * (sv - cy)) / r2, rs = Math.sqrt(s2), e = 1 - b * s2;\n let ri = rs;\n for (let n = 0; n < 4; n++) ri -= (ri * (1 + lensK1 * ri * ri) - rs) / (1 + 3 * lensK1 * ri * ri);\n value *= (1 + lensK1 * ri * ri) * (1 + 3 * lensK1 * ri * ri) / (e * e);\n }\n out[o] = value;\n }\n return out;\n}\n"]}
1
+ {"version":3,"file":"developed-lens-corrections.js","sourceRoot":"","sources":["../../src/output/developed-lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * @internal `output.lensCorrections` (#570): a catalogue lens's generic correction profile (`createGenericLensCorrectionProfile`)\n * applied to the developed linear RGB of `simulateCapture`, after demosaic, colour and white balance and before the tone curve.\n * The RAW and the DNG are never corrected.\n *\n * For an output pixel at p (pixels from the frame centre, +y up) and channel c, the source point is read by inverse mapping,\n * as the profile composes it (lateral colour, then distortion):\n * q = m · a_c · p, s = q · (1 + k1·|q|²/R²),\n * with a_c = 1 + L·D(λ_c) the channel's lateral-colour scale, k1 the lens's corner distortion, R the half-diagonal and m ≤ 1 the\n * fill factor (the largest that keeps every output pixel centre's source inside the frame for every channel, so the output keeps\n * its size with no missing pixels; the image is magnified by 1/m). The channel is sampled bilinearly at s.\n *\n * Vignetting is corrected to a flat field: the sample is multiplied by det J(ρ_i) / E(ρ_s), the inverse of the relative\n * illumination the capture delivered at s: the lens's E(ρ) = (1 − bρ²)² at the sensor point and its distortion's area factor\n * 1/det J, det J = (1 + k1ρ_i²)(1 + 3k1ρ_i²) at the ideal point ρ_i that the lens images to ρ_s (the capture's\n * \"inverse-distortion-jacobian\"; a relative illumination measured on a flat field contains it too). From profile 1.1.0 (#584) the\n * profile's vignetting component carries the same area factor (`distortionAreaFactor`), and its gain equals this kernel's.\n * Every position is computed in whole-frame pixel coordinates, so a region renders the same values as the whole frame.\n */\n\nimport { InvalidScientificResultError } from \"../core/validation.js\";\nimport type { LensCorrectionChannel } from \"./lens-corrections.js\";\nimport { genericLensDispersion } from \"../equipment/generic-lens-catalogue.js\";\n\n/**\n * Effective wavelengths of the red, green and blue channels for the lateral-colour map, nm: a generic approximation of where\n * a camera's channel responses peak, not a measured camera. The default of `createGenericLensCorrectionProfile` without a\n * `channelDispersion`; `simulateCapture`'s `output.lensCorrections` uses the capture's own channels instead (#589).\n */\nexport const GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM: Readonly<Record<LensCorrectionChannel, number>> = /* @__PURE__ */ Object.freeze({ red: 610, green: 540, blue: 465 });\n\n/** A normalized dispersion per channel (`genericLensDispersion` units: 0 at the d line, F minus C = 1). */\nexport type ChannelDispersion = Readonly<Record<LensCorrectionChannel, number>>;\n\n/**\n * @internal #589: each channel's effective dispersion D̄_c = Σ_n w_c,n·D(λ_n) / Σ_n w_c,n, w_c,n = response_c(λ_n)·illuminant(λ_n), the\n * dispersion a neutral surface's image in that channel carries (its centroid moves by L·r·D̄_c under lateral colour L·r·D(λ)).\n * `response` holds the red, green and blue values at the nodes `wavelengthsNm`; each channel needs a positive total weight.\n */\nexport function channelEffectiveDispersion(wavelengthsNm: readonly number[], response: readonly ArrayLike<number>[], illuminant: ArrayLike<number>): ChannelDispersion {\n const [red, green, blue] = response.map((r) => {\n let weighted = 0, total = 0;\n wavelengthsNm.forEach((nm, n) => { const w = r[n]! * illuminant[n]!; weighted += w * genericLensDispersion(nm); total += w; });\n return weighted / total;\n }) as [number, number, number];\n return { red, green, blue };\n}\n\n/**\n * @internal The lateral-colour scale of one channel, 1 + L·D̄ with D̄ the channel's effective dispersion, or D(λ_channel) at\n * {@link GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM} without one (`D` the catalogue's normalized dispersion).\n */\nexport function genericLensChannelScale(fraction: number, channel: LensCorrectionChannel, channels?: ChannelDispersion): number {\n return 1 + fraction * (channels ? channels[channel] : genericLensDispersion(GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM[channel]));\n}\n\n/** @internal Which corrections run. */\nexport interface DevelopedLensCorrectionSelection { distortion: boolean; lateralColor: boolean; vignetting: boolean }\n/** @internal A prepared correction for one frame (pixel units). */\nexport interface DevelopedLensCorrection {\n width: number; height: number;\n /** Half-diagonal R in pixels. */\n radius: number;\n /** k1 (0 when distortion is off), the lens's own k1, and the red, green and blue scales a_c (1 when lateral colour is off). */\n k1: number; lensK1: number; scales: readonly [number, number, number];\n /** Fill factor m ≤ 1 (1/m is the magnification). */\n fill: number;\n}\ntype Rect = { x: number; y: number; width: number; height: number };\n\n/** Source position (pixel-centre coordinates of the whole frame, unclamped) of output pixel (x, y), channel c. */\nfunction sourceOf(c: DevelopedLensCorrection, m: number, x: number, y: number, channel: number, out: Float64Array): void {\n const a = m * c.scales[channel]!, px = (x + 0.5 - c.width / 2) * a, py = (c.height / 2 - y - 0.5) * a;\n const s = 1 + c.k1 * (px * px + py * py) / (c.radius * c.radius);\n out[0] = px * s + c.width / 2 - 0.5; out[1] = c.height / 2 - 0.5 - py * s;\n}\n/** Calls `visit` for every pixel centre on the border of `r`. */\nfunction border(r: Rect, visit: (x: number, y: number) => void): void {\n for (let x = r.x; x < r.x + r.width; x++) { visit(x, r.y); visit(x, r.y + r.height - 1); }\n for (let y = r.y; y < r.y + r.height; y++) { visit(r.x, y); visit(r.x + r.width - 1, y); }\n}\n\n/**\n * @internal Prepares the correction of a W × H frame. `lens` is the resolved catalogue lens (`resolveGenericLens`) on the\n * frame; `channels` the channels' effective dispersion (#589). Without it the scales span every channel's possible dispersion\n * (red and green at D(780 nm), blue at D(380 nm), the spectral range's ends): `simulateCapture` prepares that first, so the\n * developed rectangle a region reads (`developedLensCorrectionSourceRect`) holds the sources of any channel scales. The mapping's source radius grows with the output radius (the catalogue's\n * distortion is monotonic over the frame), so the sources of the border pixel centres bound every source, and the fill factor\n * is found by bisection on them.\n */\nexport function prepareDevelopedLensCorrection(lens: { halfDiagonalMm: number; character: { distortion: { corner: number }; lateralColor: { fraction: number } } },\n selection: DevelopedLensCorrectionSelection, width: number, height: number, pitchMm: number,\n channels: ChannelDispersion = { red: genericLensDispersion(780), green: genericLensDispersion(780), blue: genericLensDispersion(380) }): DevelopedLensCorrection {\n const fraction = selection.lateralColor ? lens.character.lateralColor.fraction : 0, scale = (c: LensCorrectionChannel): number => genericLensChannelScale(fraction, c, channels);\n const c: DevelopedLensCorrection = { width, height, radius: lens.halfDiagonalMm / pitchMm,\n k1: selection.distortion ? lens.character.distortion.corner : 0, lensK1: lens.character.distortion.corner, scales: [scale(\"red\"), scale(\"green\"), scale(\"blue\")], fill: 1 };\n const p = new Float64Array(2), frame = { x: 0, y: 0, width, height };\n const fits = (m: number): boolean => {\n let ok = true;\n border(frame, (x, y) => {\n for (let k = 0; k < 3 && ok; k++) { sourceOf(c, m, x, y, k, p); ok = p[0]! >= 0 && p[0]! <= width - 1 && p[1]! >= 0 && p[1]! <= height - 1; }\n });\n return ok;\n };\n if (!fits(1)) {\n let lo = 0.5, hi = 1;\n for (let i = 0; i < 52; i++) { const mid = (lo + hi) / 2; if (fits(mid)) lo = mid; else hi = mid; }\n c.fill = lo;\n }\n return c;\n}\n\n/**\n * @internal The developed pixels (a rectangle of the whole frame) that the corrected pixels of `region` read: the bounding box of\n * the region border's sources over the three channels, with their bilinear neighbours, one pixel of margin, clipped to the frame.\n */\nexport function developedLensCorrectionSourceRect(c: DevelopedLensCorrection, region: Rect): Rect {\n const p = new Float64Array(2);\n let x0 = Infinity, y0 = Infinity, x1 = -Infinity, y1 = -Infinity;\n border(region, (x, y) => {\n for (let k = 0; k < 3; k++) {\n sourceOf(c, c.fill, x, y, k, p);\n x0 = Math.min(x0, p[0]!); x1 = Math.max(x1, p[0]!); y0 = Math.min(y0, p[1]!); y1 = Math.max(y1, p[1]!);\n }\n });\n const left = Math.max(0, Math.floor(x0) - 1), top = Math.max(0, Math.floor(y0) - 1);\n return { x: left, y: top, width: Math.min(c.width - 1, Math.floor(x1) + 2) - left + 1, height: Math.min(c.height - 1, Math.floor(y1) + 2) - top + 1 };\n}\n\n/**\n * @internal Corrects developed linear RGB (`rgb`, interleaved, the frame's pixels in `source`) into the pixels of `region`, both\n * rectangles of the whole frame. `cornerStops` is the lens's vignetting at the capture's f-number, or null to leave it.\n * Positions outside the frame (none after the fill, up to rounding) are clamped to the edge pixel centres.\n */\nexport function applyDevelopedLensCorrection(c: DevelopedLensCorrection, rgb: Float64Array, source: Rect, region: Rect, cornerStops: number | null): Float64Array {\n const out = new Float64Array(region.width * region.height * 3), W = c.width, H = c.height, cx = W / 2 - 0.5, cy = H / 2 - 0.5, r2 = c.radius * c.radius;\n const b = cornerStops === null ? 0 : 1 - 2 ** (-cornerStops / 2), k1 = c.k1, lensK1 = c.lensK1;\n for (let y = 0, o = 0; y < region.height; y++) for (let x = 0; x < region.width; x++) for (let k = 0; k < 3; k++, o++) {\n // sourceOf, inlined (a call per sample is about 5 times slower here): the same arithmetic, so the region bounds hold.\n const a = c.fill * c.scales[k]!, px = (region.x + x + 0.5 - W / 2) * a, py = (H / 2 - (region.y + y) - 0.5) * a, f = 1 + k1 * (px * px + py * py) / r2;\n const su = px * f + W / 2 - 0.5, sv = H / 2 - 0.5 - py * f;\n const u = Math.min(W - 1, Math.max(0, su)), v = Math.min(H - 1, Math.max(0, sv));\n const i = Math.floor(u), j = Math.floor(v), dx = u - i, dy = v - j, i1 = dx === 0 ? i : i + 1, j1 = dy === 0 ? j : j + 1;\n if (i < source.x || j < source.y || i1 >= source.x + source.width || j1 >= source.y + source.height)\n throw new InvalidScientificResultError(\"Lens correction read outside the developed rectangle.\");\n const row = (j - source.y) * source.width, row1 = (j1 - source.y) * source.width, a0 = i - source.x, a1 = i1 - source.x;\n let value = (1 - dy) * ((1 - dx) * rgb[(row + a0) * 3 + k]! + dx * rgb[(row + a1) * 3 + k]!) + dy * ((1 - dx) * rgb[(row1 + a0) * 3 + k]! + dx * rgb[(row1 + a1) * 3 + k]!);\n if (b !== 0) {\n // ρ_s², then ρ_i from ρ_s = ρ_i(1 + k1ρ_i²) by Newton's method (monotonic over the frame; converged to rounding in 4 steps).\n const s2 = ((su - cx) * (su - cx) + (sv - cy) * (sv - cy)) / r2, rs = Math.sqrt(s2), e = 1 - b * s2;\n let ri = rs;\n for (let n = 0; n < 4; n++) ri -= (ri * (1 + lensK1 * ri * ri) - rs) / (1 + 3 * lensK1 * ri * ri);\n value *= (1 + lensK1 * ri * ri) * (1 + 3 * lensK1 * ri * ri) / (e * e);\n }\n out[o] = value;\n }\n return out;\n}\n"]}
@@ -1,6 +1,36 @@
1
+ /**
2
+ * The generic correction profile of a catalogue lens (#549), for the post-capture lens corrections
3
+ * (`resolveLensCorrectionPlan`, `calculateLensCorrectedCapture`): a distortion map, a lateral-colour map per channel and a
4
+ * peripheral-illumination gain, each from the lens's own character (`resolveGenericLens`). Consumer lenses rely on it more:
5
+ * their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is
6
+ * a labelled approximation, bound to one exact camera state (`OpticalProfileState`).
7
+ */
8
+ import { type CalculationResult } from "../core/calculation-result.js";
1
9
  import { type OpticalProfileState } from "../optics/profile-contract.js";
2
10
  import { type GenericLensCorrectionProfile } from "./lens-corrections.js";
3
- export { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM } from "./developed-lens-corrections.js";
11
+ import { type ChannelDispersion } from "./developed-lens-corrections.js";
12
+ export { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM, type ChannelDispersion } from "./developed-lens-corrections.js";
13
+ export interface CalculateChannelEffectiveDispersionInput {
14
+ /** Spectral nodes, nm, strictly increasing, within 300–1100 nm. */
15
+ wavelengthsNm: readonly number[];
16
+ /** Each channel's response at the nodes (any common linear unit, ≥ 0; for example electrons per unit spectral radiance). */
17
+ channelResponse: {
18
+ red: readonly number[];
19
+ green: readonly number[];
20
+ blue: readonly number[];
21
+ };
22
+ /** The illuminant's relative spectral power at the nodes (≥ 0; the scale cancels). */
23
+ illuminant: readonly number[];
24
+ }
25
+ /**
26
+ * #589: each channel's effective dispersion for the catalogue lenses' lateral colour, D̄_c = Σ w·D(λ) / Σ w with
27
+ * w = response_c(λ)·illuminant(λ) at the nodes and D the normalised Cauchy dispersion (`genericLensDispersion`: 0 at the
28
+ * Fraunhofer d line, F minus C = 1). Under lateral colour L·r·D(λ) a neutral surface's image in channel c moves by L·r·D̄_c, so
29
+ * blue minus red is L·r·(D̄_blue − D̄_red). Pass the result as `channelDispersion` to `createGenericLensCorrectionProfile`.
30
+ * `simulateCapture`'s `output.lensCorrections` computes it from the capture's own channels and reports it in its assumption.
31
+ * An approximation: a weighted mean over a neutral surface; a colored surface weights the band differently.
32
+ */
33
+ export declare function calculateChannelEffectiveDispersion(input: CalculateChannelEffectiveDispersionInput): CalculationResult<ChannelDispersion>;
4
34
  export interface CreateGenericLensCorrectionProfileInput {
5
35
  /** A catalogue id (`GENERIC_LENS_IDS`); `state.lensId` must be the same id. */
6
36
  lensId: string;
@@ -11,12 +41,18 @@ export interface CreateGenericLensCorrectionProfileInput {
11
41
  };
12
42
  /** The exact camera state the profile applies to: its focal length must be the lens's on this sensor, its aperture in range. */
13
43
  state: OpticalProfileState;
44
+ /**
45
+ * #589: each channel's effective dispersion (`calculateChannelEffectiveDispersion` for the camera's channels and the light).
46
+ * Default: D at {@link GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM} (610, 540 and 465 nm), a generic approximation.
47
+ */
48
+ channelDispersion?: ChannelDispersion;
14
49
  }
15
50
  /**
16
51
  * Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own
17
- * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the
18
- * relative-illumination gain at the state's aperture (strength 1). From profile version 1.1.0 (#584) the vignetting component
19
- * (version 1.1.0) carries the distortion's area factor (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's
20
- * sensor point, so it flattens a flat-field capture. The result passes `parseGenericLensCorrectionProfile`.
52
+ * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D̄_channel (`channelDispersion`,
53
+ * by default D at the generic channel wavelengths), and the relative-illumination gain at the state's aperture (strength 1). From
54
+ * profile version 1.1.0 (#584) the vignetting component (version 1.1.0) carries the distortion's area factor
55
+ * (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's sensor point, so it flattens a flat-field capture. The
56
+ * result passes `parseGenericLensCorrectionProfile`.
21
57
  */
22
58
  export declare function createGenericLensCorrectionProfile(input: CreateGenericLensCorrectionProfileInput): GenericLensCorrectionProfile;
@@ -6,23 +6,67 @@
6
6
  * their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is
7
7
  * a labelled approximation, bound to one exact camera state (`OpticalProfileState`).
8
8
  */
9
+ import { approximationResult } from "../core/calculation-result.js";
9
10
  import { InvalidScientificInputError } from "../core/validation.js";
10
11
  import { requireInputKeys, requireInputObject } from "../core/record-validation.js";
11
12
  import { parseOpticalProfileState } from "../optics/profile-contract.js";
12
- import { GENERIC_LENS_EVIDENCE, genericLensDispersion, resolveGenericLens } from "../equipment/generic-lens-catalogue.js";
13
+ import { GENERIC_LENS_EVIDENCE, resolveGenericLens } from "../equipment/generic-lens-catalogue.js";
13
14
  import { parseGenericLensCorrectionProfile } from "./lens-corrections.js";
14
- import { genericLensChannelScale } from "./developed-lens-corrections.js";
15
+ import { channelEffectiveDispersion, genericLensChannelScale } from "./developed-lens-corrections.js";
15
16
  export { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM } from "./developed-lens-corrections.js";
17
+ /**
18
+ * #589: each channel's effective dispersion for the catalogue lenses' lateral colour, D̄_c = Σ w·D(λ) / Σ w with
19
+ * w = response_c(λ)·illuminant(λ) at the nodes and D the normalised Cauchy dispersion (`genericLensDispersion`: 0 at the
20
+ * Fraunhofer d line, F minus C = 1). Under lateral colour L·r·D(λ) a neutral surface's image in channel c moves by L·r·D̄_c, so
21
+ * blue minus red is L·r·(D̄_blue − D̄_red). Pass the result as `channelDispersion` to `createGenericLensCorrectionProfile`.
22
+ * `simulateCapture`'s `output.lensCorrections` computes it from the capture's own channels and reports it in its assumption.
23
+ * An approximation: a weighted mean over a neutral surface; a colored surface weights the band differently.
24
+ */
25
+ export function calculateChannelEffectiveDispersion(input) {
26
+ requireInputObject(input, "calculateChannelEffectiveDispersion input");
27
+ requireInputKeys(input, "calculateChannelEffectiveDispersion", ["wavelengthsNm", "channelResponse", "illuminant"]);
28
+ const nodes = input.wavelengthsNm, response = input.channelResponse;
29
+ if (!Array.isArray(nodes) || nodes.length === 0 || nodes.some((nm, n) => typeof nm !== "number" || !(nm >= 300 && nm <= 1100) || (n > 0 && !(nm > nodes[n - 1]))))
30
+ throw new InvalidScientificInputError("wavelengthsNm must be strictly increasing numbers within 300–1100 nm.");
31
+ requireInputObject(response, "channelResponse");
32
+ requireInputKeys(response, "channelResponse", ["red", "green", "blue"]);
33
+ const series = (values, name) => {
34
+ if (!Array.isArray(values) || values.length !== nodes.length || values.some((v) => typeof v !== "number" || !Number.isFinite(v) || v < 0))
35
+ throw new InvalidScientificInputError(name + " must hold one finite value ≥ 0 per wavelength node.");
36
+ return values;
37
+ };
38
+ const illuminant = series(input.illuminant, "illuminant"), channels = ["red", "green", "blue"].map((c) => series(response[c], "channelResponse." + c));
39
+ if (channels.some((r) => !(r.reduce((sum, v, n) => sum + v * illuminant[n], 0) > 0)))
40
+ throw new InvalidScientificInputError("Every channel needs a positive response under the illuminant.");
41
+ const value = channelEffectiveDispersion(nodes, channels, illuminant);
42
+ return approximationResult(value, "generic-lens-channel-effective-dispersion", "1.0.0", [
43
+ "D̄_c = Σ response_c·illuminant·D(λ) / Σ response_c·illuminant over the given nodes, for a neutral (spectrally flat) surface.",
44
+ "D(λ) is the catalogue lenses' normalised Cauchy dispersion (Fraunhofer d, F and C lines)."
45
+ ]);
46
+ }
16
47
  /**
17
48
  * Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own
18
- * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the
19
- * relative-illumination gain at the state's aperture (strength 1). From profile version 1.1.0 (#584) the vignetting component
20
- * (version 1.1.0) carries the distortion's area factor (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's
21
- * sensor point, so it flattens a flat-field capture. The result passes `parseGenericLensCorrectionProfile`.
49
+ * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D̄_channel (`channelDispersion`,
50
+ * by default D at the generic channel wavelengths), and the relative-illumination gain at the state's aperture (strength 1). From
51
+ * profile version 1.1.0 (#584) the vignetting component (version 1.1.0) carries the distortion's area factor
52
+ * (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's sensor point, so it flattens a flat-field capture. The
53
+ * result passes `parseGenericLensCorrectionProfile`.
22
54
  */
23
55
  export function createGenericLensCorrectionProfile(input) {
24
56
  requireInputObject(input, "createGenericLensCorrectionProfile input");
25
- requireInputKeys(input, "createGenericLensCorrectionProfile", ["lensId", "activeImagingArea", "state"]);
57
+ requireInputKeys(input, "createGenericLensCorrectionProfile", ["lensId", "activeImagingArea", "state", "channelDispersion"]);
58
+ let channels;
59
+ if (input.channelDispersion !== undefined) {
60
+ requireInputObject(input.channelDispersion, "channelDispersion");
61
+ requireInputKeys(input.channelDispersion, "channelDispersion", ["red", "green", "blue"]);
62
+ for (const c of ["red", "green", "blue"]) {
63
+ const v = input.channelDispersion[c];
64
+ // D spans −1.08 (1100 nm) to 4.30 (300 nm).
65
+ if (typeof v !== "number" || !(v >= -1.1 && v <= 4.4))
66
+ throw new InvalidScientificInputError("channelDispersion." + c + " must be a number in [−1.1, 4.4], D between 1100 and 300 nm.");
67
+ }
68
+ channels = input.channelDispersion;
69
+ }
26
70
  const state = parseOpticalProfileState(input.state);
27
71
  const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea, fNumber: state.aperture });
28
72
  if (state.lensId !== lens.lensId)
@@ -31,7 +75,7 @@ export function createGenericLensCorrectionProfile(input) {
31
75
  throw new InvalidScientificInputError("state.focalLengthMm must be the " + lens.name + "'s focal length on this sensor, " + lens.focalLengthMm + " mm.");
32
76
  const c = lens.character, domain = "reconstructed-linear";
33
77
  const identity = { version: "1.0.0", domain, availability: "toggle", defaultEnabled: true, dependencies: [], requiredByStabilizationModes: [] };
34
- const scale = (channel) => genericLensChannelScale(c.lateralColor.fraction, genericLensDispersion, channel);
78
+ const scale = (channel) => genericLensChannelScale(c.lateralColor.fraction, channel, channels);
35
79
  const affine = (channel) => ({ id: lens.lensId + ":lateral-ca:" + channel, version: "1.0.0", domain, purpose: "lateral-ca", kind: "affine",
36
80
  matrix: [scale(channel), 0, 0, scale(channel)], offsetMm: { x: 0, y: 0 } });
37
81
  return parseGenericLensCorrectionProfile({ schemaVersion: "0.1.0", id: "photivra-" + lens.lensId + ":corrections", version: "1.1.0", state,
@@ -1 +1 @@
1
- {"version":3,"file":"generic-lens-corrections.js","sourceRoot":"","sources":["../../src/output/generic-lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The generic correction profile of a catalogue lens (#549), for the post-capture lens corrections\n * (`resolveLensCorrectionPlan`, `calculateLensCorrectedCapture`): a distortion map, a lateral-colour map per channel and a\n * peripheral-illumination gain, each from the lens's own character (`resolveGenericLens`). Consumer lenses rely on it more:\n * their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is\n * a labelled approximation, bound to one exact camera state (`OpticalProfileState`).\n */\n\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { parseOpticalProfileState, type OpticalProfileState } from \"../optics/profile-contract.js\";\nimport { GENERIC_LENS_EVIDENCE, genericLensDispersion, resolveGenericLens } from \"../equipment/generic-lens-catalogue.js\";\nimport { parseGenericLensCorrectionProfile, type GenericLensCorrectionProfile, type LensCorrectionChannel } from \"./lens-corrections.js\";\nimport type { AffineGeometricTransform } from \"./geometric-transforms.js\";\nimport { genericLensChannelScale } from \"./developed-lens-corrections.js\";\n\nexport { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM } from \"./developed-lens-corrections.js\";\n\nexport interface CreateGenericLensCorrectionProfileInput {\n /** A catalogue id (`GENERIC_LENS_IDS`); `state.lensId` must be the same id. */\n lensId: string;\n /** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */\n activeImagingArea: { widthMm: number; heightMm: number };\n /** The exact camera state the profile applies to: its focal length must be the lens's on this sensor, its aperture in range. */\n state: OpticalProfileState;\n}\n\n/**\n * Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own\n * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D(λ_channel), and the\n * relative-illumination gain at the state's aperture (strength 1). From profile version 1.1.0 (#584) the vignetting component\n * (version 1.1.0) carries the distortion's area factor (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's\n * sensor point, so it flattens a flat-field capture. The result passes `parseGenericLensCorrectionProfile`.\n */\nexport function createGenericLensCorrectionProfile(input: CreateGenericLensCorrectionProfileInput): GenericLensCorrectionProfile {\n requireInputObject(input, \"createGenericLensCorrectionProfile input\");\n requireInputKeys(input, \"createGenericLensCorrectionProfile\", [\"lensId\", \"activeImagingArea\", \"state\"]);\n const state = parseOpticalProfileState(input.state);\n const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea, fNumber: state.aperture });\n if (state.lensId !== lens.lensId) throw new InvalidScientificInputError(\"state.lensId must be the catalogue id \" + lens.lensId + \".\");\n if (Math.abs(state.focalLengthMm - lens.focalLengthMm) > 1e-9 * lens.focalLengthMm)\n throw new InvalidScientificInputError(\"state.focalLengthMm must be the \" + lens.name + \"'s focal length on this sensor, \" + lens.focalLengthMm + \" mm.\");\n const c = lens.character, domain = \"reconstructed-linear\" as const;\n const identity = { version: \"1.0.0\", domain, availability: \"toggle\" as const, defaultEnabled: true, dependencies: [] as string[], requiredByStabilizationModes: [] as string[] };\n const scale = (channel: LensCorrectionChannel): number => genericLensChannelScale(c.lateralColor.fraction, genericLensDispersion, channel);\n const affine = (channel: LensCorrectionChannel): AffineGeometricTransform => ({ id: lens.lensId + \":lateral-ca:\" + channel, version: \"1.0.0\", domain, purpose: \"lateral-ca\" as const, kind: \"affine\" as const,\n matrix: [scale(channel), 0, 0, scale(channel)] as [number, number, number, number], offsetMm: { x: 0, y: 0 } });\n return parseGenericLensCorrectionProfile({ schemaVersion: \"0.1.0\", id: \"photivra-\" + lens.lensId + \":corrections\", version: \"1.1.0\", state,\n evidence: { kind: \"generic-parametric\", basis: \"Generic catalogue lens \" + lens.name + \" (#549): the inverse of its own distortion, lateral colour and vignetting.\",\n residualNote: \"Generic approximation, not a measured lens; axial colour, coma and the other aberrations stay.\", sources: [...GENERIC_LENS_EVIDENCE] },\n components: [\n { ...identity, id: \"distortion\", kind: \"geometry\", residualNote: \"Corrects the lens's radial distortion r_d = r(1 + k1ρ²); the stretched corners lose resolution.\",\n transform: { id: lens.lensId + \":distortion\", version: \"1.0.0\", domain, purpose: \"distortion\", kind: \"radial\",\n profile: { normalizationRadiusMm: lens.halfDiagonalMm, maximumNormalizedRadius: 1.05, coefficients: { k1: c.distortion.corner, k2: 0, k3: 0 } } } },\n { ...identity, id: \"lateral-ca\", kind: \"lateral-ca\", residualNote: \"Registers the channels at one effective wavelength each; colour inside a channel's band and axial colour stay.\",\n transforms: { red: affine(\"red\"), green: affine(\"green\"), blue: affine(\"blue\") } },\n // #584 (component 1.1.0): the gain also undoes the distortion's area factor, so a flat field comes out flat.\n { ...identity, version: \"1.1.0\", id: \"vignetting\", kind: \"peripheral-illumination\",\n residualNote: \"Gain det J/E at the capture's sensor point and the profile's aperture (E the lens's relative illumination, 1/det J its distortion's area factor); it raises corner noise with the signal.\",\n profile: { ...c.vignetting.profile, maximumNormalizedRadius: 1.05 }, strength: 1, distortionAreaFactor: { ...c.distortion.profile, coefficients: { ...c.distortion.profile.coefficients } } }\n ] });\n}\n"]}
1
+ {"version":3,"file":"generic-lens-corrections.js","sourceRoot":"","sources":["../../src/output/generic-lens-corrections.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The generic correction profile of a catalogue lens (#549), for the post-capture lens corrections\n * (`resolveLensCorrectionPlan`, `calculateLensCorrectedCapture`): a distortion map, a lateral-colour map per channel and a\n * peripheral-illumination gain, each from the lens's own character (`resolveGenericLens`). Consumer lenses rely on it more:\n * their uncorrected distortion and vignetting are larger, so the same profile corrects more. Like every catalogue number it is\n * a labelled approximation, bound to one exact camera state (`OpticalProfileState`).\n */\n\nimport { approximationResult, type CalculationResult } from \"../core/calculation-result.js\";\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { requireInputKeys, requireInputObject } from \"../core/record-validation.js\";\nimport { parseOpticalProfileState, type OpticalProfileState } from \"../optics/profile-contract.js\";\nimport { GENERIC_LENS_EVIDENCE, resolveGenericLens } from \"../equipment/generic-lens-catalogue.js\";\nimport { parseGenericLensCorrectionProfile, type GenericLensCorrectionProfile, type LensCorrectionChannel } from \"./lens-corrections.js\";\nimport type { AffineGeometricTransform } from \"./geometric-transforms.js\";\nimport { channelEffectiveDispersion, genericLensChannelScale, type ChannelDispersion } from \"./developed-lens-corrections.js\";\n\nexport { GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM, type ChannelDispersion } from \"./developed-lens-corrections.js\";\n\nexport interface CalculateChannelEffectiveDispersionInput {\n /** Spectral nodes, nm, strictly increasing, within 300–1100 nm. */\n wavelengthsNm: readonly number[];\n /** Each channel's response at the nodes (any common linear unit, ≥ 0; for example electrons per unit spectral radiance). */\n channelResponse: { red: readonly number[]; green: readonly number[]; blue: readonly number[] };\n /** The illuminant's relative spectral power at the nodes (≥ 0; the scale cancels). */\n illuminant: readonly number[];\n}\n\n/**\n * #589: each channel's effective dispersion for the catalogue lenses' lateral colour, D̄_c = Σ w·D(λ) / Σ w with\n * w = response_c(λ)·illuminant(λ) at the nodes and D the normalised Cauchy dispersion (`genericLensDispersion`: 0 at the\n * Fraunhofer d line, F minus C = 1). Under lateral colour L·r·D(λ) a neutral surface's image in channel c moves by L·r·D̄_c, so\n * blue minus red is L·r·(D̄_blue − D̄_red). Pass the result as `channelDispersion` to `createGenericLensCorrectionProfile`.\n * `simulateCapture`'s `output.lensCorrections` computes it from the capture's own channels and reports it in its assumption.\n * An approximation: a weighted mean over a neutral surface; a colored surface weights the band differently.\n */\nexport function calculateChannelEffectiveDispersion(input: CalculateChannelEffectiveDispersionInput): CalculationResult<ChannelDispersion> {\n requireInputObject(input, \"calculateChannelEffectiveDispersion input\");\n requireInputKeys(input, \"calculateChannelEffectiveDispersion\", [\"wavelengthsNm\", \"channelResponse\", \"illuminant\"]);\n const nodes = input.wavelengthsNm, response = input.channelResponse;\n if (!Array.isArray(nodes) || nodes.length === 0 || nodes.some((nm, n) => typeof nm !== \"number\" || !(nm >= 300 && nm <= 1100) || (n > 0 && !(nm > nodes[n - 1]!))))\n throw new InvalidScientificInputError(\"wavelengthsNm must be strictly increasing numbers within 300–1100 nm.\");\n requireInputObject(response, \"channelResponse\");\n requireInputKeys(response, \"channelResponse\", [\"red\", \"green\", \"blue\"]);\n const series = (values: unknown, name: string): readonly number[] => {\n if (!Array.isArray(values) || values.length !== nodes.length || values.some((v) => typeof v !== \"number\" || !Number.isFinite(v) || v < 0))\n throw new InvalidScientificInputError(name + \" must hold one finite value ≥ 0 per wavelength node.\");\n return values as number[];\n };\n const illuminant = series(input.illuminant, \"illuminant\"), channels = ([\"red\", \"green\", \"blue\"] as const).map((c) => series(response[c], \"channelResponse.\" + c));\n if (channels.some((r) => !(r.reduce((sum, v, n) => sum + v * illuminant[n]!, 0) > 0))) throw new InvalidScientificInputError(\"Every channel needs a positive response under the illuminant.\");\n const value = channelEffectiveDispersion(nodes, channels, illuminant);\n return approximationResult(value, \"generic-lens-channel-effective-dispersion\", \"1.0.0\", [\n \"D̄_c = Σ response_c·illuminant·D(λ) / Σ response_c·illuminant over the given nodes, for a neutral (spectrally flat) surface.\",\n \"D(λ) is the catalogue lenses' normalised Cauchy dispersion (Fraunhofer d, F and C lines).\"]);\n}\n\nexport interface CreateGenericLensCorrectionProfileInput {\n /** A catalogue id (`GENERIC_LENS_IDS`); `state.lensId` must be the same id. */\n lensId: string;\n /** The sensor's active capture area, mm (the lens is scaled to it as in `resolveGenericLens`). */\n activeImagingArea: { widthMm: number; heightMm: number };\n /** The exact camera state the profile applies to: its focal length must be the lens's on this sensor, its aperture in range. */\n state: OpticalProfileState;\n /**\n * #589: each channel's effective dispersion (`calculateChannelEffectiveDispersion` for the camera's channels and the light).\n * Default: D at {@link GENERIC_LENS_CORRECTION_CHANNEL_WAVELENGTHS_NM} (610, 540 and 465 nm), a generic approximation.\n */\n channelDispersion?: ChannelDispersion;\n}\n\n/**\n * Builds the catalogue lens's generic correction profile for one camera state: a radial distortion map (the lens's own\n * r_d = r(1 + k1ρ²), read at each ideal destination point), per-channel lateral-colour scales 1 + L·D̄_channel (`channelDispersion`,\n * by default D at the generic channel wavelengths), and the relative-illumination gain at the state's aperture (strength 1). From\n * profile version 1.1.0 (#584) the vignetting component (version 1.1.0) carries the distortion's area factor\n * (`distortionAreaFactor`): its gain is det J(ρ_i)/E(ρ_s) at the capture's sensor point, so it flattens a flat-field capture. The\n * result passes `parseGenericLensCorrectionProfile`.\n */\nexport function createGenericLensCorrectionProfile(input: CreateGenericLensCorrectionProfileInput): GenericLensCorrectionProfile {\n requireInputObject(input, \"createGenericLensCorrectionProfile input\");\n requireInputKeys(input, \"createGenericLensCorrectionProfile\", [\"lensId\", \"activeImagingArea\", \"state\", \"channelDispersion\"]);\n let channels: ChannelDispersion | undefined;\n if (input.channelDispersion !== undefined) {\n requireInputObject(input.channelDispersion, \"channelDispersion\");\n requireInputKeys(input.channelDispersion, \"channelDispersion\", [\"red\", \"green\", \"blue\"]);\n for (const c of [\"red\", \"green\", \"blue\"] as const) {\n const v = input.channelDispersion[c];\n // D spans −1.08 (1100 nm) to 4.30 (300 nm).\n if (typeof v !== \"number\" || !(v >= -1.1 && v <= 4.4)) throw new InvalidScientificInputError(\"channelDispersion.\" + c + \" must be a number in [−1.1, 4.4], D between 1100 and 300 nm.\");\n }\n channels = input.channelDispersion;\n }\n const state = parseOpticalProfileState(input.state);\n const lens = resolveGenericLens({ lensId: input.lensId, activeImagingArea: input.activeImagingArea, fNumber: state.aperture });\n if (state.lensId !== lens.lensId) throw new InvalidScientificInputError(\"state.lensId must be the catalogue id \" + lens.lensId + \".\");\n if (Math.abs(state.focalLengthMm - lens.focalLengthMm) > 1e-9 * lens.focalLengthMm)\n throw new InvalidScientificInputError(\"state.focalLengthMm must be the \" + lens.name + \"'s focal length on this sensor, \" + lens.focalLengthMm + \" mm.\");\n const c = lens.character, domain = \"reconstructed-linear\" as const;\n const identity = { version: \"1.0.0\", domain, availability: \"toggle\" as const, defaultEnabled: true, dependencies: [] as string[], requiredByStabilizationModes: [] as string[] };\n const scale = (channel: LensCorrectionChannel): number => genericLensChannelScale(c.lateralColor.fraction, channel, channels);\n const affine = (channel: LensCorrectionChannel): AffineGeometricTransform => ({ id: lens.lensId + \":lateral-ca:\" + channel, version: \"1.0.0\", domain, purpose: \"lateral-ca\" as const, kind: \"affine\" as const,\n matrix: [scale(channel), 0, 0, scale(channel)] as [number, number, number, number], offsetMm: { x: 0, y: 0 } });\n return parseGenericLensCorrectionProfile({ schemaVersion: \"0.1.0\", id: \"photivra-\" + lens.lensId + \":corrections\", version: \"1.1.0\", state,\n evidence: { kind: \"generic-parametric\", basis: \"Generic catalogue lens \" + lens.name + \" (#549): the inverse of its own distortion, lateral colour and vignetting.\",\n residualNote: \"Generic approximation, not a measured lens; axial colour, coma and the other aberrations stay.\", sources: [...GENERIC_LENS_EVIDENCE] },\n components: [\n { ...identity, id: \"distortion\", kind: \"geometry\", residualNote: \"Corrects the lens's radial distortion r_d = r(1 + k1ρ²); the stretched corners lose resolution.\",\n transform: { id: lens.lensId + \":distortion\", version: \"1.0.0\", domain, purpose: \"distortion\", kind: \"radial\",\n profile: { normalizationRadiusMm: lens.halfDiagonalMm, maximumNormalizedRadius: 1.05, coefficients: { k1: c.distortion.corner, k2: 0, k3: 0 } } } },\n { ...identity, id: \"lateral-ca\", kind: \"lateral-ca\", residualNote: \"Registers the channels at one effective wavelength each; colour inside a channel's band and axial colour stay.\",\n transforms: { red: affine(\"red\"), green: affine(\"green\"), blue: affine(\"blue\") } },\n // #584 (component 1.1.0): the gain also undoes the distortion's area factor, so a flat field comes out flat.\n { ...identity, version: \"1.1.0\", id: \"vignetting\", kind: \"peripheral-illumination\",\n residualNote: \"Gain det J/E at the capture's sensor point and the profile's aperture (E the lens's relative illumination, 1/det J its distortion's area factor); it raises corner noise with the signal.\",\n profile: { ...c.vignetting.profile, maximumNormalizedRadius: 1.05 }, strength: 1, distortionAreaFactor: { ...c.distortion.profile, coefficients: { ...c.distortion.profile.coefficients } } }\n ] });\n}\n"]}
@@ -11,7 +11,7 @@ import { freezeOwnedData } from "../core/owned-data.js";
11
11
  import { FULL_RESOLUTION_CEILING_PIXELS, resolveRaisedLimit } from "../core/pixel-limits.js";
12
12
  import { encodeSrgbComponent } from "../color/srgb-transfer.js";
13
13
  import { InvalidScientificResultError } from "../core/validation.js";
14
- import { parseSdrRenderingProfile, prepareSdrPixelTone } from "./sdr-rendering.js";
14
+ import { desaturateToLuminance, parseSdrRenderingProfile, prepareSdrPixelTone } from "./sdr-rendering.js";
15
15
  export const NATIVE_CAPTURE_SDR_SCHEMA_VERSION = "0.1.0";
16
16
  /** `maximumPixels` is the default pixel limit; a request may raise it with its own `maximumPixels`, up to `ceilingPixels`. */
17
17
  export const NATIVE_CAPTURE_SDR_LIMITS = /* @__PURE__ */ Object.freeze({ maximumPixels: 24_000_000, ceilingPixels: FULL_RESOLUTION_CEILING_PIXELS,
@@ -187,7 +187,7 @@ export function nativeSrgb8QuantizerForTest() {
187
187
  * values. Not exported from the package.
188
188
  */
189
189
  export function renderSdrRunInto(source, start, pixelCount, profile, tone, scale, maximum, codes, outOffset, scratch, counts) {
190
- const reject = profile.gamutHandling === "reject-out-of-range", quantizer = maximum === 255 ? srgb8Quantizer() : null;
190
+ const reject = profile.gamutHandling === "reject-out-of-range", desaturate = profile.gamutHandling === "desaturate-to-luminance", quantizer = maximum === 255 ? srgb8Quantizer() : null;
191
191
  let negative = 0, above = 0, changed = 0, low = 0, high = 0, outOfGamut = false;
192
192
  for (let p = 0, i = start, o = outOffset; p < pixelCount; p++, i += 3) {
193
193
  // The reference parser normalizes -0 to +0 before exposure.
@@ -196,6 +196,9 @@ export function renderSdrRunInto(source, start, pixelCount, profile, tone, scale
196
196
  if (!Number.isFinite(er) || !Number.isFinite(eg) || !Number.isFinite(eb))
197
197
  throw new InvalidScientificResultError("Rendering exposure produced a non-finite sample.");
198
198
  tone(er, eg, eb, scratch);
199
+ // #601: the hue-preserving desaturation, before the components are counted and clipped.
200
+ if (desaturate)
201
+ desaturateToLuminance(scratch);
199
202
  for (let c = 0; c < 3; c++, o++) {
200
203
  const exposed = c === 0 ? er : c === 1 ? eg : eb;
201
204
  if (exposed < 0)