@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
@@ -0,0 +1,10 @@
1
+ /**
2
+ * #595 (2.22.0): `result.sensor.native`, the sensor figures of one native photosite, in a module of its own so that it can be loaded
3
+ * apart from the capture's default path: the same report at the native pitch, the EMVA 1288 Release 4.0 dynamic range (§2.7,
4
+ * Eq. 26–28) and the 8 MP-normalized one (a convention). Model figures, exact arithmetic under the engine's noise model.
5
+ * @see docs/QUICK_START.md, "Native-pixel dynamic range (#595)".
6
+ */
7
+ import type { SensorFixedPatternNoiseProfile } from "./fixed-pattern-noise.js";
8
+ import { type NativeSensorInput, type SimulateCaptureNativeSensorReport, type SimulateCaptureSensorReport } from "./sensor-characteristics.js";
9
+ /** The native figures for a capture's report `rendered` (its regime, grey charge and dark charge). */
10
+ export declare function nativeSensorReport(rendered: SimulateCaptureSensorReport, regimeId: string, greyElectrons: number | undefined, darkElectrons: number, native: NativeSensorInput, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureNativeSensorReport;
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ import { regimeRange, sensorReport } from "./sensor-characteristics.js";
3
+ /** The native figures for a capture's report `rendered` (its regime, grey charge and dark charge). */
4
+ export function nativeSensorReport(rendered, regimeId, greyElectrons, darkElectrons, native, pattern) {
5
+ const base = { pixelPitchMicrometers: rendered.pixelPitchMicrometers, peakQuantumEfficiency: rendered.peakQuantumEfficiency, readoutSeconds: rendered.readoutSeconds };
6
+ // #595: the same report at the native pitch; EMVA 1288 R4.0 Eq. (27) μ_e.min = √(σ_d² + σ_q²/K² + 1/4) + 1/2 and Eq. (28) DR = μ_e.sat / μ_e.min.
7
+ const p0 = native.pixelPitchMicrometers, area = (p0 / base.pixelPitchMicrometers) ** 2, own = sensorReport(native.profile, regimeId, native.capacityElectrons, greyElectrons && greyElectrons * area, darkElectrons, { ...base, pixelPitchMicrometers: p0 }, pattern);
8
+ const q = regimeRange(native.profile, native.profile.regimes.find((r) => r.regimeId === regimeId), native.capacityElectrons);
9
+ const threshold = Math.sqrt(q.read ** 2 + q.quantization ** 2 + 0.25) + 0.5, emva = Math.log2(q.saturation / threshold), normalization = 0.5 * Math.log2(native.framePhotosites / 8e6);
10
+ return { pixelPitchMicrometers: p0, photositesPerSite: 1 / area, fullWellElectrons: native.capacityElectrons, readNoiseElectrons: q.read, dynamicRangeStops: own.dynamicRangeStops,
11
+ dynamicRangeByIso: own.dynamicRangeByIso, emva1288: { absoluteSensitivityThresholdElectrons: threshold, dynamicRangeStops: emva, dynamicRangeDecibels: 20 * Math.log10(2) * emva,
12
+ saturationCapacityElectronsPerSquareMicrometer: q.saturation / (p0 * p0) },
13
+ normalized8Megapixel: { referencePixels: 8e6, framePhotosites: native.framePhotosites, dynamicRangeStops: own.dynamicRangeStops + normalization, emva1288DynamicRangeStops: emva + normalization },
14
+ ...(own.midGrey ? { midGrey: own.midGrey } : {}) };
15
+ }
16
+ //# sourceMappingURL=native-sensor-report.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"native-sensor-report.js","sourceRoot":"","sources":["../../src/sensor/native-sensor-report.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAUtC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAoG,MAAM,6BAA6B,CAAC;AAE1K,sGAAsG;AACtG,MAAM,UAAU,kBAAkB,CAAC,QAAqC,EAAE,QAAgB,EAAE,aAAiC,EAAE,aAAqB,EAAE,MAAyB,EAC7K,OAAwC;IACxC,MAAM,IAAI,GAAG,EAAE,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB,EAAE,qBAAqB,EAAE,QAAQ,CAAC,qBAAqB,EAAE,cAAc,EAAE,QAAQ,CAAC,cAAc,EAAE,CAAC;IACvK,kJAAkJ;IAClJ,MAAM,EAAE,GAAG,MAAM,CAAC,qBAAqB,EAAE,IAAI,GAAG,CAAC,EAAE,GAAG,IAAI,CAAC,qBAAqB,CAAC,IAAI,CAAC,EAAE,GAAG,GAAG,YAAY,CAAC,MAAM,CAAC,OAAO,EAAE,QAAQ,EAAE,MAAM,CAAC,iBAAiB,EACzJ,aAAa,IAAI,aAAa,GAAG,IAAI,EAAE,aAAa,EAAE,EAAE,GAAG,IAAI,EAAE,qBAAqB,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,CAAC;IAC3G,MAAM,CAAC,GAAG,WAAW,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,KAAK,QAAQ,CAAE,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;IAC9H,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,YAAY,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,GAAG,EAAE,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,GAAG,SAAS,CAAC,EAAE,aAAa,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,eAAe,GAAG,GAAG,CAAC,CAAC;IACvL,OAAO,EAAE,qBAAqB,EAAE,EAAE,EAAE,iBAAiB,EAAE,CAAC,GAAG,IAAI,EAAE,iBAAiB,EAAE,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,EAAE,CAAC,CAAC,IAAI,EAAE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB;QAChL,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,EAAE,QAAQ,EAAE,EAAE,qCAAqC,EAAE,SAAS,EAAE,iBAAiB,EAAE,IAAI,EAAE,oBAAoB,EAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,IAAI;YAC9K,8CAA8C,EAAE,CAAC,CAAC,UAAU,GAAG,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE;QAC5E,oBAAoB,EAAE,EAAE,eAAe,EAAE,GAAG,EAAE,eAAe,EAAE,MAAM,CAAC,eAAe,EAAE,iBAAiB,EAAE,GAAG,CAAC,iBAAiB,GAAG,aAAa,EAAE,yBAAyB,EAAE,IAAI,GAAG,aAAa,EAAE;QAClM,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC;AACvD,CAAC","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * #595 (2.22.0): `result.sensor.native`, the sensor figures of one native photosite, in a module of its own so that it can be loaded\n * apart from the capture's default path: the same report at the native pitch, the EMVA 1288 Release 4.0 dynamic range (§2.7,\n * Eq. 26–28) and the 8 MP-normalized one (a convention). Model figures, exact arithmetic under the engine's noise model.\n * @see docs/QUICK_START.md, \"Native-pixel dynamic range (#595)\".\n */\n\nimport type { SensorFixedPatternNoiseProfile } from \"./fixed-pattern-noise.js\";\nimport { regimeRange, sensorReport, type NativeSensorInput, type SimulateCaptureNativeSensorReport, type SimulateCaptureSensorReport } from \"./sensor-characteristics.js\";\n\n/** The native figures for a capture's report `rendered` (its regime, grey charge and dark charge). */\nexport function nativeSensorReport(rendered: SimulateCaptureSensorReport, regimeId: string, greyElectrons: number | undefined, darkElectrons: number, native: NativeSensorInput,\n pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureNativeSensorReport {\n const base = { pixelPitchMicrometers: rendered.pixelPitchMicrometers, peakQuantumEfficiency: rendered.peakQuantumEfficiency, readoutSeconds: rendered.readoutSeconds };\n // #595: the same report at the native pitch; EMVA 1288 R4.0 Eq. (27) μ_e.min = √(σ_d² + σ_q²/K² + 1/4) + 1/2 and Eq. (28) DR = μ_e.sat / μ_e.min.\n const p0 = native.pixelPitchMicrometers, area = (p0 / base.pixelPitchMicrometers) ** 2, own = sensorReport(native.profile, regimeId, native.capacityElectrons,\n greyElectrons && greyElectrons * area, darkElectrons, { ...base, pixelPitchMicrometers: p0 }, pattern);\n const q = regimeRange(native.profile, native.profile.regimes.find((r) => r.regimeId === regimeId)!, native.capacityElectrons);\n const threshold = Math.sqrt(q.read ** 2 + q.quantization ** 2 + 0.25) + 0.5, emva = Math.log2(q.saturation / threshold), normalization = 0.5 * Math.log2(native.framePhotosites / 8e6);\n return { pixelPitchMicrometers: p0, photositesPerSite: 1 / area, fullWellElectrons: native.capacityElectrons, readNoiseElectrons: q.read, dynamicRangeStops: own.dynamicRangeStops,\n dynamicRangeByIso: own.dynamicRangeByIso, emva1288: { absoluteSensitivityThresholdElectrons: threshold, dynamicRangeStops: emva, dynamicRangeDecibels: 20 * Math.log10(2) * emva,\n saturationCapacityElectronsPerSquareMicrometer: q.saturation / (p0 * p0) },\n normalized8Megapixel: { referencePixels: 8e6, framePhotosites: native.framePhotosites, dynamicRangeStops: own.dynamicRangeStops + normalization, emva1288DynamicRangeStops: emva + normalization },\n ...(own.midGrey ? { midGrey: own.midGrey } : {}) };\n}\n"]}
@@ -1,4 +1,4 @@
1
- import { type SensorReadoutConversionProfile } from "./raw-readout.js";
1
+ import { type SensorReadoutConversionProfile, type SensorReadoutConversionRegime } from "./raw-readout.js";
2
2
  import type { SensorFixedPatternNoiseProfile } from "./fixed-pattern-noise.js";
3
3
  /**
4
4
  * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an
@@ -24,7 +24,9 @@ export interface SimulateCaptureSensorCharacteristics {
24
24
  referencePixelPitchMicrometers?: number;
25
25
  };
26
26
  /**
27
- * Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from
27
+ * Read noise, RMS e⁻ (0–100), of one physical photosite, as a sensor datasheet gives it (2.22.0: on a tier too; a rendered site that
28
+ * sums N photosites, above the tier's 6 µm or a profile's own pitch, takes it ×√N), of the low and the high conversion gain, and
29
+ * the dual-gain switch: the ISO setting from
28
30
  * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).
29
31
  * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).
30
32
  */
@@ -103,20 +105,88 @@ export interface SimulateCaptureSensorReport {
103
105
  * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for
104
106
  * DNG, JPEG and PNG exports; omitted for a fast request).
105
107
  */
106
- midGrey?: {
107
- reflectance: 0.18;
108
- channel: "green";
109
- signalElectrons: number;
110
- noiseElectrons: SimulateCaptureNoiseComponents;
111
- snr: number;
112
- snrDecibels: number;
113
- clipped: boolean;
108
+ midGrey?: SimulateCaptureMidGreyFigure;
109
+ /**
110
+ * #595 (2.22.0): the same figures for one of the body's native photosites, whatever pitch the capture renders (the fields above
111
+ * are the as-rendered site's). The native pitch is a generic tier's (or a caller preset's) declared 6 µm reference, or a sensor
112
+ * profile's own photosite; the request's sensor characteristics apply as they would at that pitch. Exact arithmetic under the
113
+ * engine's model (model figures, not measurements); see docs/QUICK_START.md, "Native-pixel dynamic range (#595)".
114
+ */
115
+ native: SimulateCaptureNativeSensorReport;
116
+ }
117
+ /** #535: the mid-grey noise figure of {@link SimulateCaptureSensorReport.midGrey}. */
118
+ export interface SimulateCaptureMidGreyFigure {
119
+ reflectance: 0.18;
120
+ channel: "green";
121
+ signalElectrons: number;
122
+ noiseElectrons: SimulateCaptureNoiseComponents;
123
+ snr: number;
124
+ snrDecibels: number;
125
+ clipped: boolean;
126
+ }
127
+ /** #595: {@link SimulateCaptureSensorReport.native}, the figures of one native photosite. */
128
+ export interface SimulateCaptureNativeSensorReport {
129
+ /** The native photosite pitch, µm (tiers: 6; a profile: its own). */
130
+ pixelPitchMicrometers: number;
131
+ /** N = (rendered pitch / native pitch)²: the photosites one rendered site stands for (below 1, a site smaller than a photosite). */
132
+ photositesPerSite: number;
133
+ /** Full-well capacity of one photosite, e⁻. */
134
+ fullWellElectrons: number;
135
+ /** Read noise of one photosite at the capture's ISO, RMS e⁻ (the capture's readout regime). */
136
+ readNoiseElectrons: number;
137
+ /** Engineering dynamic range of one photosite at the capture's ISO, stops (the definition of `dynamicRangeStops`). */
138
+ dynamicRangeStops: number;
139
+ /** The same per ISO setting of the body. */
140
+ dynamicRangeByIso: readonly {
141
+ iso: number;
142
+ stops: number;
143
+ }[];
144
+ /**
145
+ * EMVA 1288 Release 4.0 (§2.7, Eq. 26–28) at the capture's ISO: the absolute sensitivity threshold μ_e.min = √(σ_d² + σ_q² + 1/4) + 1/2
146
+ * (σ_d the read noise, dark current excluded; σ_q the quantization noise in e⁻), DR = μ_e.sat / μ_e.min in stops and dB, and the
147
+ * saturation capacity per area μ_e.sat / p² (Table 2).
148
+ */
149
+ emva1288: {
150
+ absoluteSensitivityThresholdElectrons: number;
151
+ dynamicRangeStops: number;
152
+ dynamicRangeDecibels: number;
153
+ saturationCapacityElectronsPerSquareMicrometer: number;
114
154
  };
155
+ /**
156
+ * A convention, not part of EMVA 1288: the dynamic range of the whole frame's native photosites resampled to 8 million output
157
+ * pixels, each summing framePhotosites / 8e6 photosites with their read noise (EMVA 1288 R4.0 §6.4, binning):
158
+ * DR + ½·log₂(framePhotosites / 8e6), for the engineering and the EMVA figure. No ADC limit is applied to the sum.
159
+ */
160
+ normalized8Megapixel: {
161
+ referencePixels: 8_000_000;
162
+ framePhotosites: number;
163
+ dynamicRangeStops: number;
164
+ emva1288DynamicRangeStops: number;
165
+ };
166
+ /** The mid-grey figure for one photosite (its charge (native/rendered pitch)² of the site's), present when `midGrey` is. */
167
+ midGrey?: SimulateCaptureMidGreyFigure;
168
+ }
169
+ /** #595: what {@link sensorReport} needs for the native figures: the readout as characterized at the native pitch. */
170
+ export interface NativeSensorInput {
171
+ profile: SensorReadoutConversionProfile;
172
+ capacityElectrons: number;
173
+ pixelPitchMicrometers: number;
174
+ framePhotosites: number;
115
175
  }
116
176
  /** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */
117
177
  export type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & {
118
178
  qeScale: number;
119
179
  };
180
+ /**
181
+ * A field's check: [minimum, maximum] (a number), the values allowed, 0 (a nested record) or a predicate. #554 shares the checker
182
+ * with the sensor profile parser.
183
+ */
184
+ export type FieldRange = readonly [number, number] | readonly unknown[] | 0 | ((value: unknown) => boolean);
185
+ /**
186
+ * Checks `keys` of `record` against `ranges` (by path below `root`; nested records by "a.b"). An undefined field is skipped, or
187
+ * with `required` refused, and with `required` every field `ranges` lists at that level is checked.
188
+ */
189
+ export declare function checkFieldRanges(record: Record<string, unknown>, keys: readonly string[], prefix: string, ranges?: Readonly<Record<string, FieldRange>>, root?: string, required?: boolean): void;
120
190
  /** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */
121
191
  export declare function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined;
122
192
  /** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */
@@ -126,12 +196,20 @@ export declare function declaredReadNoise(profile: SensorReadoutConversionProfil
126
196
  * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
127
197
  * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
128
198
  * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
129
- * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
199
+ * conversion gain's from `highGainFromIso`; every read noise (the body's or the request's) times `readNoiseScale` (#599: √N for a site
200
+ * that sums N photosites, as the sensor-profile path does). Codes per unit charge of the base regime otherwise, digital gain and the ISO
130
201
  * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
131
202
  */
132
- export declare function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile;
203
+ export declare function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics, readNoiseScale?: number): SensorReadoutConversionProfile;
204
+ /** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */
205
+ export declare function regimeRange(profile: SensorReadoutConversionProfile, regime: SensorReadoutConversionRegime, capacityElectrons: number): {
206
+ saturation: number;
207
+ quantization: number;
208
+ read: number;
209
+ };
133
210
  /**
134
211
  * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
135
- * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
212
+ * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge. `native` (#595) is added by
213
+ * `nativeSensorReport` (native-sensor-report.ts).
136
214
  */
137
215
  export declare function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number, base: Pick<SimulateCaptureSensorReport, "pixelPitchMicrometers" | "peakQuantumEfficiency" | "readoutSeconds">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport;
@@ -19,21 +19,32 @@ const RANGES = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, "fullWell.elec
19
19
  "fullWell.link": ["pixel-area", "none"], "fullWell.referencePixelPitchMicrometers": [0.5, 1000], readNoise: 0, "readNoise.lowGainElectrons": [0, 100],
20
20
  "readNoise.highGainElectrons": [0, 100], "readNoise.highGainFromIso": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };
21
21
  function fail(message) { throw new InvalidConfigurationError(message); }
22
- function check(record, keys, prefix) {
23
- for (const key of keys) {
24
- const path = prefix + key, value = record[key], range = RANGES[path];
25
- if (value === undefined)
22
+ /**
23
+ * Checks `keys` of `record` against `ranges` (by path below `root`; nested records by "a.b"). An undefined field is skipped, or
24
+ * with `required` refused, and with `required` every field `ranges` lists at that level is checked.
25
+ */
26
+ export function checkFieldRanges(record, keys, prefix, ranges = RANGES, root = "sensor.", required = false) {
27
+ const all = required ? [...new Set([...keys, ...Object.keys(ranges).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes(".")).map((k) => k.slice(prefix.length))])] : keys;
28
+ for (const key of all) {
29
+ const path = prefix + key, value = record[key], range = ranges[path];
30
+ if (value === undefined && !required)
26
31
  continue;
27
32
  if (range === undefined)
28
- fail("Unknown sensor." + prefix.slice(0, -1) + " field: " + key + ".");
33
+ fail("Unknown " + (root + prefix).slice(0, -1) + " field: " + key + ".");
34
+ if (value === undefined)
35
+ fail(root + path + " is required.");
29
36
  if (range === 0) {
30
- requireInputObject(value, "sensor." + path);
31
- check(value, Object.keys(value), path + ".");
37
+ requireInputObject(value, root + path);
38
+ checkFieldRanges(value, Object.keys(value), path + ".", ranges, root, required);
39
+ }
40
+ else if (typeof range === "function") {
41
+ if (!range(value))
42
+ fail(root + path + " is invalid.");
32
43
  }
33
44
  else {
34
45
  const choice = range.length > 2 || typeof range[0] !== "number", [minimum, maximum] = range;
35
46
  if (choice ? !range.includes(value) : typeof value !== "number" || !(value >= minimum && value <= maximum)) {
36
- fail("sensor." + path + " must be " + (choice ? range.join(" or ") : "a number from " + minimum + " to " + maximum) + ".");
47
+ fail(root + path + " must be " + (choice ? range.map(String).join(" or ") : "a number from " + minimum + " to " + maximum) + ".");
37
48
  }
38
49
  }
39
50
  }
@@ -42,7 +53,7 @@ function check(record, keys, prefix) {
42
53
  export function parseSensorCharacteristics(sensor) {
43
54
  if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined))
44
55
  return undefined;
45
- check(sensor, SENSOR_CHARACTERISTIC_KEYS, "");
56
+ checkFieldRanges(sensor, SENSOR_CHARACTERISTIC_KEYS, "");
46
57
  const c = sensor, fullWell = c.fullWell;
47
58
  if (fullWell && fullWell.electrons === undefined)
48
59
  fail("sensor.fullWell.electrons is required.");
@@ -61,12 +72,13 @@ export function declaredReadNoise(profile) {
61
72
  * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the
62
73
  * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1
63
74
  * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high
64
- * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO
75
+ * conversion gain's from `highGainFromIso`; every read noise (the body's or the request's) times `readNoiseScale` (#599: √N for a site
76
+ * that sums N photosites, as the sensor-profile path does). Codes per unit charge of the base regime otherwise, digital gain and the ISO
65
77
  * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.
66
78
  */
67
- export function characterizeReadout(profile, chargeScale, c) {
79
+ export function characterizeReadout(profile, chargeScale, c, readNoiseScale = 1) {
68
80
  const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);
69
- if (chargeScale === 1 && !bits && !n)
81
+ if (chargeScale === 1 && !bits && !n && readNoiseScale === 1)
70
82
  return profile;
71
83
  return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {
72
84
  const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;
@@ -78,15 +90,17 @@ export function characterizeReadout(profile, chargeScale, c) {
78
90
  if (n) {
79
91
  const gain = regime.exposureIndexGain ?? fail("sensor.readNoise needs readout regimes that declare their exposure index.");
80
92
  const from = n.highGainFromIso ?? declared.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;
81
- const value = high ? n.highGainElectrons ?? declared.highGainElectrons : n.lowGainElectrons ?? declared.lowGainElectrons;
93
+ const value = (high ? n.highGainElectrons ?? declared.highGainElectrons : n.lowGainElectrons ?? declared.lowGainElectrons) * readNoiseScale;
82
94
  out.readNoiseComponents = [{ componentId: "electronic", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];
83
95
  out.exposureIndexGain = { ...gain, conversionGainMode: high ? "high-conversion-gain" : "low-conversion-gain" };
84
96
  }
97
+ else if (readNoiseScale !== 1)
98
+ out.readNoiseComponents = regime.readNoiseComponents.map((component) => ({ ...component, rmsElectrons: { ...component.rmsElectrons, value: component.rmsElectrons.value * readNoiseScale } }));
85
99
  return out;
86
100
  }) });
87
101
  }
88
102
  /** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */
89
- function regimeRange(profile, regime, capacityElectrons) {
103
+ export function regimeRange(profile, regime, capacityElectrons) {
90
104
  const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;
91
105
  const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,
92
106
  systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);
@@ -96,7 +110,8 @@ function regimeRange(profile, regime, capacityElectrons) {
96
110
  }
97
111
  /**
98
112
  * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the
99
- * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.
113
+ * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge. `native` (#595) is added by
114
+ * `nativeSensorReport` (native-sensor-report.ts).
100
115
  */
101
116
  export function sensorReport(profile, regimeId, capacityElectrons, greyElectrons, darkElectrons, base, pattern) {
102
117
  const stops = (r) => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };
@@ -1 +1 @@
1
- {"version":3,"file":"sensor-characteristics.js","sourceRoot":"","sources":["../../src/sensor/sensor-characteristics.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request\n * options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic\n * range and mid-grey signal-to-noise ratio the result reports.\n *\n * Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's\n * preset record alike, and a later sensor profile (#554) can supply the same options as data.\n * @see docs/QUICK_START.md, \"Sensor characteristics (#555)\", for the options, the equations and their sources.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateSiteEffectiveSaturationCode } from \"../capture/raw-white-level.js\";\nimport { parseSensorReadoutConversionProfile, type SensorReadoutConversionProfile, type SensorReadoutConversionRegime } from \"./raw-readout.js\";\nimport type { SensorFixedPatternNoiseProfile } from \"./fixed-pattern-noise.js\";\n\n/**\n * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an\n * omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.\n * Every value set is listed in the result's `assumptions`.\n */\nexport interface SimulateCaptureSensorCharacteristics {\n /**\n * Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's\n * idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their\n * shape (and the colour matrix is unchanged) and the collected charge scales in proportion.\n */\n peakQuantumEfficiency?: number;\n /**\n * Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation\n * at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `\"pixel-area\"` (default)\n * scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `\"none\"`\n * uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.\n */\n fullWell?: { electrons: number; link?: \"pixel-area\" | \"none\"; referencePixelPitchMicrometers?: number };\n /**\n * Read noise, RMS e⁻ (0–100), of the low and the high conversion gain, and the dual-gain switch: the ISO setting from\n * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).\n * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).\n */\n readNoise?: { lowGainElectrons?: number; highGainElectrons?: number; highGainFromIso?: number };\n /** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */\n readoutSeconds?: number;\n /**\n * ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range\n * (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.\n */\n adcBitDepth?: 12 | 14 | 16;\n /**\n * Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA\n * 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.\n */\n fixedPatternNoise?: boolean;\n}\n\n/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */\nexport const SENSOR_CHARACTERISTIC_KEYS = [\"peakQuantumEfficiency\", \"fullWell\", \"readNoise\", \"readoutSeconds\", \"adcBitDepth\", \"fixedPatternNoise\"] as const;\n\n/** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */\nexport interface SimulateCaptureNoiseComponents {\n /** Photon shot noise √S (Poisson). */\n photonShot: number;\n /** Read noise of the capture's readout regime. */\n read: number;\n /** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */\n darkCurrent: number;\n /** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */\n quantization: number;\n /** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */\n fixedPattern?: number;\n /** √ of the sum of the squares. */\n total: number;\n}\n\n/**\n * #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).\n * Model figures from the engine's own noise model, not measurements.\n */\nexport interface SimulateCaptureSensorReport {\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (at 555 nm). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** The body's rolling readout skew, s (null when it declares no rolling readout). */\n readoutSeconds: number | null;\n /** ADC bit depth of the capture's readout regime. */\n adcBitDepth: number;\n fixedPatternNoise: boolean;\n /**\n * Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white\n * level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and\n * ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal\n * noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.\n */\n dynamicRangeStops: number;\n /** The same per ISO setting of the body, in its order. */\n dynamicRangeByIso: readonly { iso: number; stops: number }[];\n /**\n * #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance\n * at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).\n * `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and\n * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for\n * DNG, JPEG and PNG exports; omitted for a fast request).\n */\n midGrey?: { reflectance: 0.18; channel: \"green\"; signalElectrons: number; noiseElectrons: SimulateCaptureNoiseComponents; snr: number; snrDecibels: number; clipped: boolean };\n}\n\n/** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */\nexport type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & { qeScale: number };\n\n/** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */\nconst RANGES: Readonly<Record<string, readonly [number, number] | readonly unknown[] | 0>> = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, \"fullWell.electrons\": [100, 1e10],\n \"fullWell.link\": [\"pixel-area\", \"none\"], \"fullWell.referencePixelPitchMicrometers\": [0.5, 1000], readNoise: 0, \"readNoise.lowGainElectrons\": [0, 100],\n \"readNoise.highGainElectrons\": [0, 100], \"readNoise.highGainFromIso\": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\nfunction check(record: Record<string, unknown>, keys: readonly string[], prefix: string): void {\n for (const key of keys) {\n const path = prefix + key, value = record[key], range = RANGES[path];\n if (value === undefined) continue;\n if (range === undefined) fail(\"Unknown sensor.\" + prefix.slice(0, -1) + \" field: \" + key + \".\");\n if (range === 0) {\n requireInputObject(value, \"sensor.\" + path);\n check(value as Record<string, unknown>, Object.keys(value as object), path + \".\");\n } else {\n const choice = range.length > 2 || typeof range[0] !== \"number\", [minimum, maximum] = range as readonly [number, number];\n if (choice ? !(range as readonly unknown[]).includes(value) : typeof value !== \"number\" || !(value >= minimum && value <= maximum)) {\n fail(\"sensor.\" + path + \" must be \" + (choice ? range.join(\" or \") : \"a number from \" + minimum + \" to \" + maximum) + \".\");\n }\n }\n }\n}\n\n/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */\nexport function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined {\n if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined)) return undefined;\n check(sensor, SENSOR_CHARACTERISTIC_KEYS, \"\");\n const c = sensor as SimulateCaptureSensorCharacteristics, fullWell = c.fullWell;\n if (fullWell && fullWell.electrons === undefined) fail(\"sensor.fullWell.electrons is required.\");\n if (fullWell?.link === \"none\" && fullWell.referencePixelPitchMicrometers !== undefined) fail(\"sensor.fullWell.referencePixelPitchMicrometers applies only to link \\\"pixel-area\\\".\");\n return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };\n}\n\nconst noiseOf = (regime: SensorReadoutConversionRegime): number => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));\n/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */\nexport function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport[\"readNoise\"] {\n const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === \"high-conversion-gain\"), low = noiseOf(regimes[0]!);\n return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain!.exposureIndex : null };\n}\n\n/**\n * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,\n * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the\n * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1\n * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high\n * conversion gain's from `highGainFromIso`. Codes per unit charge of the base regime otherwise, digital gain and the ISO\n * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.\n */\nexport function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics): SensorReadoutConversionProfile {\n const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);\n if (chargeScale === 1 && !bits && !n) return profile;\n return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {\n const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;\n const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;\n const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?\n regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },\n preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },\n ...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };\n if (n) {\n const gain = regime.exposureIndexGain ?? fail(\"sensor.readNoise needs readout regimes that declare their exposure index.\");\n const from = n.highGainFromIso ?? declared!.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;\n const value = high ? n.highGainElectrons ?? declared!.highGainElectrons : n.lowGainElectrons ?? declared!.lowGainElectrons;\n out.readNoiseComponents = [{ componentId: \"electronic\", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];\n out.exposureIndexGain = { ...gain, conversionGainMode: high ? \"high-conversion-gain\" : \"low-conversion-gain\" };\n }\n return out;\n }) });\n}\n\n/** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */\nfunction regimeRange(profile: SensorReadoutConversionProfile, regime: SensorReadoutConversionRegime, capacityElectrons: number): { saturation: number; quantization: number; read: number } {\n const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;\n const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,\n systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);\n // Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.\n const perCode = gain / digitalGain;\n return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };\n}\n\n/**\n * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the\n * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge.\n */\nexport function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number,\n base: Pick<SimulateCaptureSensorReport, \"pixelPitchMicrometers\" | \"peakQuantumEfficiency\" | \"readoutSeconds\">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport {\n const stops = (r: SensorReadoutConversionRegime): number => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };\n const regime = profile.regimes.find((r) => r.regimeId === regimeId)!, range = regimeRange(profile, regime, capacityElectrons);\n const report: SimulateCaptureSensorReport = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,\n dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) };\n if (greyElectrons === undefined) return report;\n const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons,\n pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);\n const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };\n noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);\n const snr = greyElectrons / noise.total;\n report.midGrey = { reflectance: 0.18, channel: \"green\", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };\n return report;\n}\n"]}
1
+ {"version":3,"file":"sensor-characteristics.js","sourceRoot":"","sources":["../../src/sensor/sensor-characteristics.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Adjustable sensor characteristics for `simulateCapture` (#555) and the capture's noise figure (#535): the request\n * options that replace a camera body's sensor values one at a time, the readout they give, and the calculated dynamic\n * range and mid-grey signal-to-noise ratio the result reports.\n *\n * Everything here works on the body's declared readout regimes, so it applies to a generic tier and to a caller's\n * preset record alike, and a later sensor profile (#554) can supply the same options as data.\n * @see docs/QUICK_START.md, \"Sensor characteristics (#555)\", for the options, the equations and their sources.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateSiteEffectiveSaturationCode } from \"../capture/raw-white-level.js\";\nimport { parseSensorReadoutConversionProfile, type SensorReadoutConversionProfile, type SensorReadoutConversionRegime } from \"./raw-readout.js\";\nimport type { SensorFixedPatternNoiseProfile } from \"./fixed-pattern-noise.js\";\n\n/**\n * #555: sensor characteristics a capture can set one at a time (`simulateCapture({ sensor })`). Each is off by default: an\n * omitted field keeps the camera body's value, and a request with none of them is byte-identical to one before 2.20.0.\n * Every value set is listed in the result's `assumptions`.\n */\nexport interface SimulateCaptureSensorCharacteristics {\n /**\n * Peak external quantum efficiency of the green channel (its value at 555 nm), 0.05–0.9. Default 0.5, the facade's\n * idealized sensor. Every channel's EQE curve is multiplied by `peakQuantumEfficiency / 0.5`, so the curves keep their\n * shape (and the colour matrix is unchanged) and the collected charge scales in proportion.\n */\n peakQuantumEfficiency?: number;\n /**\n * Full-well capacity: the charge, e⁻, that reaches the RAW white level at base ISO. Default: the body's base-ISO saturation\n * at 6 µm, scaled by the pixel area (the `sensor.isoAreaNormalization` approximation). `link` `\"pixel-area\"` (default)\n * scales `electrons`, the full well at `referencePixelPitchMicrometers` (default 6), by (pitch / reference)²; `\"none\"`\n * uses `electrons` at any pitch. The result is 100 e⁻ to 1e10 e⁻.\n */\n fullWell?: { electrons: number; link?: \"pixel-area\" | \"none\"; referencePixelPitchMicrometers?: number };\n /**\n * Read noise, RMS e⁻ (0–100), of one physical photosite, as a sensor datasheet gives it (2.22.0: on a tier too; a rendered site that\n * sums N photosites, above the tier's 6 µm or a profile's own pitch, takes it ×√N), of the low and the high conversion gain, and\n * the dual-gain switch: the ISO setting from\n * which the high conversion gain is used (any ISO from 100; above the body's highest ISO, the low gain is used throughout).\n * An omitted field keeps the body's value. Needs readout regimes that declare their exposure index (the tiers do).\n */\n readNoise?: { lowGainElectrons?: number; highGainElectrons?: number; highGainFromIso?: number };\n /** Readout time, s (1e-6 to 1): the rolling shutter's top-to-bottom skew. Default: the body's (tiers 8, 15 and 30 ms). */\n readoutSeconds?: number;\n /**\n * ADC bit depth: 12, 14 or 16. Default: the body's (consumer 12, the others 14). The black level scales with the code range\n * (×2^Δbits), and the conversion gain follows so the same charge reaches the same fraction of the range.\n */\n adcBitDepth?: 12 | 14 | 16;\n /**\n * Opt-in (advanced): the body's synthetic fixed-pattern noise profile (PRNU, DSNU, hot pixels, row and column offsets; EMVA\n * 1288 Release 4.0 non-uniformities), the same pattern in every capture. Default false. Not in `simulateDisplayView`.\n */\n fixedPatternNoise?: boolean;\n}\n\n/** The option keys of {@link SimulateCaptureSensorCharacteristics}, in `sensor`. */\nexport const SENSOR_CHARACTERISTIC_KEYS = [\"peakQuantumEfficiency\", \"fullWell\", \"readNoise\", \"readoutSeconds\", \"adcBitDepth\", \"fixedPatternNoise\"] as const;\n\n/** One RMS noise component of the mid-grey figure, e⁻ (input-referred). */\nexport interface SimulateCaptureNoiseComponents {\n /** Photon shot noise √S (Poisson). */\n photonShot: number;\n /** Read noise of the capture's readout regime. */\n read: number;\n /** Dark-current shot noise √(D·t), D = 1 e⁻/s at 20 °C. */\n darkCurrent: number;\n /** ADC quantization, one code (and one more after a digital gain) as a uniform 1/12 code² variance, in e⁻. */\n quantization: number;\n /** With `sensor.fixedPatternNoise`: √((PRNU·S)² + (DSNU·D·t)² + row² + column²), hot pixels excluded; a spatial, not temporal, noise. */\n fixedPattern?: number;\n /** √ of the sum of the squares. */\n total: number;\n}\n\n/**\n * #555, #535: the capture's sensor characteristics as used, and the figures calculated from them (reported, not settable).\n * Model figures from the engine's own noise model, not measurements.\n */\nexport interface SimulateCaptureSensorReport {\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (at 555 nm). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the base-ISO saturation charge (`sensor.capacityElectrons`). */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻, of each conversion gain, and the ISO from which the high gain is used (null: one gain throughout). */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** The body's rolling readout skew, s (null when it declares no rolling readout). */\n readoutSeconds: number | null;\n /** ADC bit depth of the capture's readout regime. */\n adcBitDepth: number;\n fixedPatternNoise: boolean;\n /**\n * Engineering dynamic range at the capture's ISO, stops: log₂(S_sat / σ_floor), with S_sat the charge at the RAW white\n * level (full well over the analog gain, limited by the charge capacity and the ADC range) and σ_floor the read noise and\n * ADC quantization added in quadrature (the temporal dark noise of EMVA 1288 Release 4.0 without dark current). An ideal\n * noiseless readout is limited by the ADC alone: log₂((white − black)·√12) for unit digital gain.\n */\n dynamicRangeStops: number;\n /** The same per ISO setting of the body, in its order. */\n dynamicRangeByIso: readonly { iso: number; stops: number }[];\n /**\n * #535: the expected signal-to-noise ratio of an 18 % Lambertian grey patch under the request's illuminant and illuminance\n * at the capture's exposure, ISO and sensor, on the green channel next to the optical axis (no vignetting, PSF or motion).\n * `snr` = S/σ_total and `snrDecibels` = 20·log₁₀(snr). `clipped`: S is at or above the white level, where the RAW clips and\n * the figure no longer describes the image. Computed only when the request resolves to exact (`fast: false`, the default for\n * DNG, JPEG and PNG exports; omitted for a fast request).\n */\n midGrey?: SimulateCaptureMidGreyFigure;\n /**\n * #595 (2.22.0): the same figures for one of the body's native photosites, whatever pitch the capture renders (the fields above\n * are the as-rendered site's). The native pitch is a generic tier's (or a caller preset's) declared 6 µm reference, or a sensor\n * profile's own photosite; the request's sensor characteristics apply as they would at that pitch. Exact arithmetic under the\n * engine's model (model figures, not measurements); see docs/QUICK_START.md, \"Native-pixel dynamic range (#595)\".\n */\n native: SimulateCaptureNativeSensorReport;\n}\n\n/** #535: the mid-grey noise figure of {@link SimulateCaptureSensorReport.midGrey}. */\nexport interface SimulateCaptureMidGreyFigure { reflectance: 0.18; channel: \"green\"; signalElectrons: number; noiseElectrons: SimulateCaptureNoiseComponents; snr: number; snrDecibels: number; clipped: boolean }\n\n/** #595: {@link SimulateCaptureSensorReport.native}, the figures of one native photosite. */\nexport interface SimulateCaptureNativeSensorReport {\n /** The native photosite pitch, µm (tiers: 6; a profile: its own). */\n pixelPitchMicrometers: number;\n /** N = (rendered pitch / native pitch)²: the photosites one rendered site stands for (below 1, a site smaller than a photosite). */\n photositesPerSite: number;\n /** Full-well capacity of one photosite, e⁻. */\n fullWellElectrons: number;\n /** Read noise of one photosite at the capture's ISO, RMS e⁻ (the capture's readout regime). */\n readNoiseElectrons: number;\n /** Engineering dynamic range of one photosite at the capture's ISO, stops (the definition of `dynamicRangeStops`). */\n dynamicRangeStops: number;\n /** The same per ISO setting of the body. */\n dynamicRangeByIso: readonly { iso: number; stops: number }[];\n /**\n * EMVA 1288 Release 4.0 (§2.7, Eq. 26–28) at the capture's ISO: the absolute sensitivity threshold μ_e.min = √(σ_d² + σ_q² + 1/4) + 1/2\n * (σ_d the read noise, dark current excluded; σ_q the quantization noise in e⁻), DR = μ_e.sat / μ_e.min in stops and dB, and the\n * saturation capacity per area μ_e.sat / p² (Table 2).\n */\n emva1288: { absoluteSensitivityThresholdElectrons: number; dynamicRangeStops: number; dynamicRangeDecibels: number; saturationCapacityElectronsPerSquareMicrometer: number };\n /**\n * A convention, not part of EMVA 1288: the dynamic range of the whole frame's native photosites resampled to 8 million output\n * pixels, each summing framePhotosites / 8e6 photosites with their read noise (EMVA 1288 R4.0 §6.4, binning):\n * DR + ½·log₂(framePhotosites / 8e6), for the engineering and the EMVA figure. No ADC limit is applied to the sum.\n */\n normalized8Megapixel: { referencePixels: 8_000_000; framePhotosites: number; dynamicRangeStops: number; emva1288DynamicRangeStops: number };\n /** The mid-grey figure for one photosite (its charge (native/rendered pitch)² of the site's), present when `midGrey` is. */\n midGrey?: SimulateCaptureMidGreyFigure;\n}\n\n/** #595: what {@link sensorReport} needs for the native figures: the readout as characterized at the native pitch. */\nexport interface NativeSensorInput { profile: SensorReadoutConversionProfile; capacityElectrons: number; pixelPitchMicrometers: number; framePhotosites: number }\n\n/** The request's characteristics, range-checked, with the EQE scale peakQuantumEfficiency / 0.5. */\nexport type ParsedSensorCharacteristics = SimulateCaptureSensorCharacteristics & { qeScale: number };\n\n/**\n * A field's check: [minimum, maximum] (a number), the values allowed, 0 (a nested record) or a predicate. #554 shares the checker\n * with the sensor profile parser.\n */\nexport type FieldRange = readonly [number, number] | readonly unknown[] | 0 | ((value: unknown) => boolean);\n/** Accepted ranges (design limits) and values of each characteristic, by path below `sensor`. */\nconst RANGES: Readonly<Record<string, FieldRange>> = { peakQuantumEfficiency: [0.05, 0.9], fullWell: 0, \"fullWell.electrons\": [100, 1e10],\n \"fullWell.link\": [\"pixel-area\", \"none\"], \"fullWell.referencePixelPitchMicrometers\": [0.5, 1000], readNoise: 0, \"readNoise.lowGainElectrons\": [0, 100],\n \"readNoise.highGainElectrons\": [0, 100], \"readNoise.highGainFromIso\": [100, 1e7], readoutSeconds: [1e-6, 1], adcBitDepth: [12, 14, 16], fixedPatternNoise: [true, false] };\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\n/**\n * Checks `keys` of `record` against `ranges` (by path below `root`; nested records by \"a.b\"). An undefined field is skipped, or\n * with `required` refused, and with `required` every field `ranges` lists at that level is checked.\n */\nexport function checkFieldRanges(record: Record<string, unknown>, keys: readonly string[], prefix: string, ranges: Readonly<Record<string, FieldRange>> = RANGES, root = \"sensor.\", required = false): void {\n const all = required ? [...new Set([...keys, ...Object.keys(ranges).filter((k) => k.startsWith(prefix) && !k.slice(prefix.length).includes(\".\")).map((k) => k.slice(prefix.length))])] : keys;\n for (const key of all) {\n const path = prefix + key, value = record[key], range = ranges[path];\n if (value === undefined && !required) continue;\n if (range === undefined) fail(\"Unknown \" + (root + prefix).slice(0, -1) + \" field: \" + key + \".\");\n if (value === undefined) fail(root + path + \" is required.\");\n if (range === 0) {\n requireInputObject(value, root + path);\n checkFieldRanges(value as Record<string, unknown>, Object.keys(value as object), path + \".\", ranges, root, required);\n } else if (typeof range === \"function\") {\n if (!range(value)) fail(root + path + \" is invalid.\");\n } else {\n const choice = range.length > 2 || typeof range[0] !== \"number\", [minimum, maximum] = range as readonly [number, number];\n if (choice ? !(range as readonly unknown[]).includes(value) : typeof value !== \"number\" || !(value >= minimum && value <= maximum)) {\n fail(root + path + \" must be \" + (choice ? range.map(String).join(\" or \") : \"a number from \" + minimum + \" to \" + maximum) + \".\");\n }\n }\n }\n}\n\n/** Checks the characteristics of the request's `sensor` record (its other keys already checked); undefined when it sets none. */\nexport function parseSensorCharacteristics(sensor: Record<string, unknown>): ParsedSensorCharacteristics | undefined {\n if (!SENSOR_CHARACTERISTIC_KEYS.some((key) => sensor[key] !== undefined)) return undefined;\n checkFieldRanges(sensor, SENSOR_CHARACTERISTIC_KEYS, \"\");\n const c = sensor as SimulateCaptureSensorCharacteristics, fullWell = c.fullWell;\n if (fullWell && fullWell.electrons === undefined) fail(\"sensor.fullWell.electrons is required.\");\n if (fullWell?.link === \"none\" && fullWell.referencePixelPitchMicrometers !== undefined) fail(\"sensor.fullWell.referencePixelPitchMicrometers applies only to link \\\"pixel-area\\\".\");\n return { ...c, qeScale: (c.peakQuantumEfficiency ?? 0.5) / 0.5 };\n}\n\nconst noiseOf = (regime: SensorReadoutConversionRegime): number => Math.hypot(...regime.readNoiseComponents.map((c) => c.rmsElectrons.value));\n/** The body's dual-gain read noise: the base regime's, and the first high-conversion-gain regime's with its ISO (none: the low gain throughout). */\nexport function declaredReadNoise(profile: SensorReadoutConversionProfile): SimulateCaptureSensorReport[\"readNoise\"] {\n const regimes = profile.regimes, high = regimes.find((r) => r.exposureIndexGain?.conversionGainMode === \"high-conversion-gain\"), low = noiseOf(regimes[0]!);\n return { lowGainElectrons: low, highGainElectrons: high ? noiseOf(high) : low, highGainFromIso: high ? high.exposureIndexGain!.exposureIndex : null };\n}\n\n/**\n * The readout profile with the characteristics applied: every regime's charge-domain transfer (system conversion gain,\n * e⁻ per code, and pre-ADC saturation, e⁻ equivalent) multiplied by `chargeScale`, so a full well `chargeScale` times the\n * body's reaches the same white code; with `adcBitDepth`, the black level ×2^Δbits (rounded), the saturation code 2^bits − 1\n * and the conversion gain over the new code span; with `readNoise`, one electronic read-noise component per regime, the high\n * conversion gain's from `highGainFromIso`; every read noise (the body's or the request's) times `readNoiseScale` (#599: √N for a site\n * that sums N photosites, as the sensor-profile path does). Codes per unit charge of the base regime otherwise, digital gain and the ISO\n * selection are unchanged. Re-checked by the readout parser. With nothing to change, the profile itself.\n */\nexport function characterizeReadout(profile: SensorReadoutConversionProfile, chargeScale: number, c?: ParsedSensorCharacteristics, readNoiseScale = 1): SensorReadoutConversionProfile {\n const bits = c?.adcBitDepth, n = c?.readNoise, declared = n && declaredReadNoise(profile);\n if (chargeScale === 1 && !bits && !n && readNoiseScale === 1) return profile;\n return parseSensorReadoutConversionProfile({ ...profile, regimes: profile.regimes.map((regime) => {\n const adc = regime.adc, rescale = bits !== undefined && bits !== adc.bitDepth;\n const black = rescale ? Math.round(adc.blackLevelCode * 2 ** (bits - adc.bitDepth)) : adc.blackLevelCode, saturation = rescale ? 2 ** bits - 1 : adc.digitalSaturationCode;\n const out = { ...regime, systemConversionGainElectronsPerCode: { ...regime.systemConversionGainElectronsPerCode, value: rescale ?\n regime.systemConversionGainElectronsPerCode.value * chargeScale * (adc.digitalSaturationCode - adc.blackLevelCode) / (saturation - black) : regime.systemConversionGainElectronsPerCode.value * chargeScale },\n preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * chargeScale },\n ...(rescale ? { adc: { ...adc, bitDepth: bits, blackLevelCode: black, digitalSaturationCode: saturation } } : {}) };\n if (n) {\n const gain = regime.exposureIndexGain ?? fail(\"sensor.readNoise needs readout regimes that declare their exposure index.\");\n const from = n.highGainFromIso ?? declared!.highGainFromIso ?? Infinity, high = gain.exposureIndex >= from;\n const value = (high ? n.highGainElectrons ?? declared!.highGainElectrons : n.lowGainElectrons ?? declared!.lowGainElectrons) * readNoiseScale;\n out.readNoiseComponents = [{ componentId: \"electronic\", rmsElectrons: { value, evidence: regime.evidence }, evidence: regime.evidence }];\n out.exposureIndexGain = { ...gain, conversionGainMode: high ? \"high-conversion-gain\" : \"low-conversion-gain\" };\n } else if (readNoiseScale !== 1) out.readNoiseComponents = regime.readNoiseComponents.map((component) => ({ ...component, rmsElectrons: { ...component.rmsElectrons, value: component.rmsElectrons.value * readNoiseScale } }));\n return out;\n }) });\n}\n\n/** Charge at the RAW white level and the read-plus-quantization floor of one regime, e⁻. */\nexport function regimeRange(profile: SensorReadoutConversionProfile, regime: SensorReadoutConversionRegime, capacityElectrons: number): { saturation: number; quantization: number; read: number } {\n const adc = regime.adc, gain = regime.systemConversionGainElectronsPerCode.value, digitalGain = adc.digitalGain ?? 1;\n const white = calculateSiteEffectiveSaturationCode({ physicalChargeCapacityElectrons: capacityElectrons, preAdcSaturationElectronEquivalent: regime.preAdcSaturationElectronEquivalent.value,\n systemConversionGainElectronsPerCode: gain, blackLevelCode: adc.blackLevelCode, digitalSaturationCode: adc.digitalSaturationCode, adcMaximumCode: 2 ** adc.bitDepth - 1, digitalGain }, profile, regime.regimeId);\n // Output codes per electron d/G; quantization 1/12 code² of the ADC times d², plus 1/12 of the rounding after a digital gain.\n const perCode = gain / digitalGain;\n return { saturation: (white - adc.blackLevelCode) * perCode, quantization: perCode * Math.sqrt(digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12)), read: noiseOf(regime) };\n}\n\n/**\n * The report: the characteristics used, the dynamic range per ISO, and the mid-grey noise figure for `greyElectrons`, the\n * green channel's expected charge from an 18 % grey patch, after a `darkElectrons` dark charge. `native` (#595) is added by\n * `nativeSensorReport` (native-sensor-report.ts).\n */\nexport function sensorReport(profile: SensorReadoutConversionProfile, regimeId: string, capacityElectrons: number, greyElectrons: number | undefined, darkElectrons: number,\n base: Pick<SimulateCaptureSensorReport, \"pixelPitchMicrometers\" | \"peakQuantumEfficiency\" | \"readoutSeconds\">, pattern?: SensorFixedPatternNoiseProfile): SimulateCaptureSensorReport {\n const stops = (r: SensorReadoutConversionRegime): number => { const q = regimeRange(profile, r, capacityElectrons); return Math.log2(q.saturation / Math.hypot(q.read, q.quantization)); };\n const regime = profile.regimes.find((r) => r.regimeId === regimeId)!, range = regimeRange(profile, regime, capacityElectrons);\n const report = { ...base, fullWellElectrons: capacityElectrons, readNoise: declaredReadNoise(profile), adcBitDepth: regime.adc.bitDepth, fixedPatternNoise: !!pattern,\n dynamicRangeStops: stops(regime), dynamicRangeByIso: profile.regimes.map((r) => ({ iso: r.exposureIndexGain?.exposureIndex ?? NaN, stops: stops(r) })).filter((e) => e.iso > 0) } as unknown as SimulateCaptureSensorReport;\n if (greyElectrons === undefined) return report;\n const fixedPattern = pattern && Math.hypot(pattern.photoResponseNonUniformity.relativeStandardDeviation.value * greyElectrons,\n pattern.darkSignalNonUniformity.relativeStandardDeviation.value * darkElectrons, pattern.rowOffset.rmsElectrons.value, pattern.columnOffset.rmsElectrons.value);\n const noise = { photonShot: Math.sqrt(greyElectrons), read: range.read, darkCurrent: Math.sqrt(darkElectrons), quantization: range.quantization, ...(fixedPattern === undefined ? {} : { fixedPattern }), total: 0 };\n noise.total = Math.hypot(noise.photonShot, noise.read, noise.darkCurrent, noise.quantization, fixedPattern ?? 0);\n const snr = greyElectrons / noise.total;\n report.midGrey = { reflectance: 0.18, channel: \"green\", signalElectrons: greyElectrons, noiseElectrons: noise, snr, snrDecibels: 20 * Math.log10(snr || Number.MIN_VALUE), clipped: greyElectrons + darkElectrons >= range.saturation };\n return report;\n}\n"]}
@@ -46,6 +46,7 @@ one-call capture, see the [Quick Start](QUICK_START.md). For every export, see t
46
46
  | Sensor noise and signal-to-noise ratio (SNR) | [`calculateSignalToNoise`](api/sensor-signal-noise.md#calculatesignaltonoise) | Shot noise plus read noise, in electrons. |
47
47
  | Colour temperature to a white point | [`calculateChromaticityFromCorrelatedColorTemperature`](api/color-illuminants.md#calculatechromaticityfromcorrelatedcolortemperature) | Kelvin and Duv (tint) to CIE 1931 xy. |
48
48
  | White point to colour temperature | [`calculateCorrelatedColorTemperature`](api/color-illuminants.md#calculatecorrelatedcolortemperature) | CIE 1931 xy to kelvin and Duv. |
49
+ | Fluorescent or LED light with realistic color rendering | [`createRepresentativeLightSourceIlluminant`](api/facade-representative-light-sources.md#createrepresentativelightsourceilluminant) | Photivra's own representative spectra (not named products), passed as `illuminant`; same-CCT LEDs render colors differently. Use `sensor.spectralStepNanometers: 5`. |
49
50
  | White balance from Kelvin and tint | [`createColorTemperatureWhiteBalanceIntent`](api/color-white-balance.md#createcolortemperaturewhitebalanceintent), then [`resolveColorTemperatureWhiteBalance`](api/color-white-balance.md#resolvecolortemperaturewhitebalance) | Needs an explicit camera colour matrix. In `simulateCapture()`, use `output.whiteBalance: { kelvin, tint }` instead. |
50
51
 
51
52
  If a task is not in this table, search the [API reference](API_REFERENCE.md) or the