@photivra/engine 2.19.0 → 2.21.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 (242) hide show
  1. package/CHANGELOG.md +289 -1
  2. package/CITATION.cff +1 -1
  3. package/NODE_SUPPORT.md +7 -5
  4. package/README.md +7 -6
  5. package/SECURITY.md +1 -1
  6. package/THIRD_PARTY.md +21 -10
  7. package/dist/capture/dng-lens-correction-opcodes.d.ts +32 -0
  8. package/dist/capture/dng-lens-correction-opcodes.js +119 -0
  9. package/dist/capture/dng-lens-correction-opcodes.js.map +1 -0
  10. package/dist/capture/exposure-build-up-stage.d.ts +16 -0
  11. package/dist/capture/exposure-build-up-stage.js +4 -0
  12. package/dist/capture/exposure-build-up-stage.js.map +1 -0
  13. package/dist/capture/native-environment-lean.d.ts +2 -1
  14. package/dist/capture/native-environment-lean.js +42 -78
  15. package/dist/capture/native-environment-lean.js.map +1 -1
  16. package/dist/capture/native-environment-tabulated.js +4 -3
  17. package/dist/capture/native-environment-tabulated.js.map +1 -1
  18. package/dist/capture/native-raw-dng.d.ts +6 -0
  19. package/dist/capture/native-raw-dng.js +16 -2
  20. package/dist/capture/native-raw-dng.js.map +1 -1
  21. package/dist/capture/native-raw.d.ts +3 -2
  22. package/dist/capture/native-raw.js +5 -4
  23. package/dist/capture/native-raw.js.map +1 -1
  24. package/dist/capture/sensor-raw-producer.d.ts +4 -1
  25. package/dist/capture/sensor-raw-producer.js +15 -8
  26. package/dist/capture/sensor-raw-producer.js.map +1 -1
  27. package/dist/core/shared-helpers.d.ts +2 -0
  28. package/dist/core/shared-helpers.js +4 -0
  29. package/dist/core/shared-helpers.js.map +1 -1
  30. package/dist/core/version.d.ts +1 -1
  31. package/dist/core/version.js +1 -1
  32. package/dist/core/version.js.map +1 -1
  33. package/dist/equipment/generic-lens-catalogue.d.ts +11 -4
  34. package/dist/equipment/generic-lens-catalogue.js +49 -7
  35. package/dist/equipment/generic-lens-catalogue.js.map +1 -1
  36. package/dist/equipment/generic-tier-assets.d.ts +32 -0
  37. package/dist/equipment/generic-tier-assets.js +18 -14
  38. package/dist/equipment/generic-tier-assets.js.map +1 -1
  39. package/dist/equipment/generic-tier-presets.js +2 -3
  40. package/dist/equipment/generic-tier-presets.js.map +1 -1
  41. package/dist/equipment/sensor-profile-body.d.ts +78 -0
  42. package/dist/equipment/sensor-profile-body.js +77 -0
  43. package/dist/equipment/sensor-profile-body.js.map +1 -0
  44. package/dist/equipment/sensor-profiles.d.ts +8 -0
  45. package/dist/equipment/sensor-profiles.js +82 -0
  46. package/dist/equipment/sensor-profiles.js.map +1 -0
  47. package/dist/facade/catalogue-lens-signal.d.ts +2 -2
  48. package/dist/facade/catalogue-lens-signal.js +58 -71
  49. package/dist/facade/catalogue-lens-signal.js.map +1 -1
  50. package/dist/facade/chart-radiance-batch.js +2 -1
  51. package/dist/facade/chart-radiance-batch.js.map +1 -1
  52. package/dist/facade/defocus-image-space-provider.d.ts +7 -0
  53. package/dist/facade/defocus-image-space-provider.js +64 -76
  54. package/dist/facade/defocus-image-space-provider.js.map +1 -1
  55. package/dist/facade/display-view-gpu-session.js +2 -0
  56. package/dist/facade/display-view-gpu-session.js.map +1 -1
  57. package/dist/facade/display-view-gpu.d.ts +13 -5
  58. package/dist/facade/display-view-gpu.js +4 -4
  59. package/dist/facade/display-view-gpu.js.map +1 -1
  60. package/dist/facade/display-view-lens-signal.d.ts +1 -1
  61. package/dist/facade/display-view-lens-signal.js +30 -26
  62. package/dist/facade/display-view-lens-signal.js.map +1 -1
  63. package/dist/facade/display-view.d.ts +3 -1
  64. package/dist/facade/display-view.js +6 -24
  65. package/dist/facade/display-view.js.map +1 -1
  66. package/dist/facade/expected-signal-setup.d.ts +3 -1
  67. package/dist/facade/expected-signal-setup.js +1 -1
  68. package/dist/facade/expected-signal-setup.js.map +1 -1
  69. package/dist/facade/expected-signal.d.ts +47 -14
  70. package/dist/facade/expected-signal.js +117 -112
  71. package/dist/facade/expected-signal.js.map +1 -1
  72. package/dist/facade/exposure-build-up.d.ts +70 -0
  73. package/dist/facade/exposure-build-up.js +164 -0
  74. package/dist/facade/exposure-build-up.js.map +1 -0
  75. package/dist/facade/fast-methods.d.ts +50 -0
  76. package/dist/facade/fast-methods.js +58 -0
  77. package/dist/facade/fast-methods.js.map +1 -0
  78. package/dist/facade/fast-resolution.d.ts +12 -0
  79. package/dist/facade/fast-resolution.js +18 -0
  80. package/dist/facade/fast-resolution.js.map +1 -0
  81. package/dist/facade/generic-lens-capture.d.ts +5 -4
  82. package/dist/facade/generic-lens-capture.js +5 -4
  83. package/dist/facade/generic-lens-capture.js.map +1 -1
  84. package/dist/facade/image-plane-enable.d.ts +7 -0
  85. package/dist/facade/image-plane-enable.js +17 -0
  86. package/dist/facade/image-plane-enable.js.map +1 -0
  87. package/dist/facade/image-plane-lattice-data.d.ts +6 -0
  88. package/dist/facade/image-plane-lattice-data.js +10 -0
  89. package/dist/facade/image-plane-lattice-data.js.map +1 -0
  90. package/dist/facade/image-plane-support.d.ts +33 -0
  91. package/dist/facade/image-plane-support.js +11 -0
  92. package/dist/facade/image-plane-support.js.map +1 -0
  93. package/dist/facade/image-plane.d.ts +36 -0
  94. package/dist/facade/image-plane.js +353 -0
  95. package/dist/facade/image-plane.js.map +1 -0
  96. package/dist/facade/mixed-illumination.d.ts +108 -0
  97. package/dist/facade/mixed-illumination.js +174 -0
  98. package/dist/facade/mixed-illumination.js.map +1 -0
  99. package/dist/facade/representative-light-sources.d.ts +72 -0
  100. package/dist/facade/representative-light-sources.js +124 -0
  101. package/dist/facade/representative-light-sources.js.map +1 -0
  102. package/dist/facade/scene-radiance.d.ts +129 -4
  103. package/dist/facade/scene-radiance.js +48 -13
  104. package/dist/facade/scene-radiance.js.map +1 -1
  105. package/dist/facade/simulate-capture.d.ts +88 -19
  106. package/dist/facade/simulate-capture.js +374 -145
  107. package/dist/facade/simulate-capture.js.map +1 -1
  108. package/dist/facade/simulate-display-view.d.ts +3 -2
  109. package/dist/facade/simulate-display-view.js +36 -18
  110. package/dist/facade/simulate-display-view.js.map +1 -1
  111. package/dist/gpu/chart-composite-gpu.d.ts +13 -1
  112. package/dist/gpu/chart-composite-gpu.js +48 -9
  113. package/dist/gpu/chart-composite-gpu.js.map +1 -1
  114. package/dist/gpu/chart-radiance-gpu.js +4 -30
  115. package/dist/gpu/chart-radiance-gpu.js.map +1 -1
  116. package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
  117. package/dist/gpu/display-view-composite-material-module.js +39 -13
  118. package/dist/gpu/display-view-composite-material-module.js.map +1 -1
  119. package/dist/gpu/display-view-image-material-module.d.ts +86 -0
  120. package/dist/gpu/display-view-image-material-module.js +339 -0
  121. package/dist/gpu/display-view-image-material-module.js.map +1 -0
  122. package/dist/gpu/display-view-image-quality.d.ts +45 -0
  123. package/dist/gpu/display-view-image-quality.js +380 -0
  124. package/dist/gpu/display-view-image-quality.js.map +1 -0
  125. package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
  126. package/dist/gpu/display-view-quality-gpu.js +136 -205
  127. package/dist/gpu/display-view-quality-gpu.js.map +1 -1
  128. package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
  129. package/dist/gpu/display-view-speed-gpu.js +131 -93
  130. package/dist/gpu/display-view-speed-gpu.js.map +1 -1
  131. package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
  132. package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
  133. package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
  134. package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
  135. package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
  136. package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
  137. package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
  138. package/dist/gpu/wgsl-exact-sum.js +42 -0
  139. package/dist/gpu/wgsl-exact-sum.js.map +1 -0
  140. package/dist/index.d.ts +18 -3
  141. package/dist/index.js +12 -1
  142. package/dist/index.js.map +1 -1
  143. package/dist/motion/subject-motion.d.ts +66 -0
  144. package/dist/motion/subject-motion.js +52 -0
  145. package/dist/motion/subject-motion.js.map +1 -0
  146. package/dist/optics/aperture-diameter.d.ts +47 -0
  147. package/dist/optics/aperture-diameter.js +50 -0
  148. package/dist/optics/aperture-diameter.js.map +1 -0
  149. package/dist/optics/detail-limit.d.ts +182 -0
  150. package/dist/optics/detail-limit.js +336 -0
  151. package/dist/optics/detail-limit.js.map +1 -0
  152. package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
  153. package/dist/optics/generic-lens-lateral-color.js +77 -0
  154. package/dist/optics/generic-lens-lateral-color.js.map +1 -0
  155. package/dist/optics/generic-lens-psf.d.ts +18 -2
  156. package/dist/optics/generic-lens-psf.js +79 -48
  157. package/dist/optics/generic-lens-psf.js.map +1 -1
  158. package/dist/optics/lens-psf-profile.d.ts +7 -0
  159. package/dist/optics/lens-psf-profile.js +2 -1
  160. package/dist/optics/lens-psf-profile.js.map +1 -1
  161. package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
  162. package/dist/optics/scene-to-sensor-irradiance.js +7 -0
  163. package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
  164. package/dist/output/crop-to-match-focal-length.d.ts +62 -0
  165. package/dist/output/crop-to-match-focal-length.js +63 -0
  166. package/dist/output/crop-to-match-focal-length.js.map +1 -0
  167. package/dist/output/developed-lens-corrections.d.ts +19 -5
  168. package/dist/output/developed-lens-corrections.js +30 -11
  169. package/dist/output/developed-lens-corrections.js.map +1 -1
  170. package/dist/output/generic-lens-corrections.d.ts +41 -3
  171. package/dist/output/generic-lens-corrections.js +57 -9
  172. package/dist/output/generic-lens-corrections.js.map +1 -1
  173. package/dist/output/lens-corrections.d.ts +12 -2
  174. package/dist/output/lens-corrections.js +22 -7
  175. package/dist/output/lens-corrections.js.map +1 -1
  176. package/dist/output/print-contrast-reference.js +1 -1
  177. package/dist/output/print-contrast-reference.js.map +1 -1
  178. package/dist/output/print-plan.d.ts +41 -5
  179. package/dist/output/print-plan.js +59 -14
  180. package/dist/output/print-plan.js.map +1 -1
  181. package/dist/output/print-size-blur-noise.d.ts +110 -0
  182. package/dist/output/print-size-blur-noise.js +138 -0
  183. package/dist/output/print-size-blur-noise.js.map +1 -0
  184. package/dist/sensor/binomial-sample.d.ts +3 -0
  185. package/dist/sensor/binomial-sample.js +86 -0
  186. package/dist/sensor/binomial-sample.js.map +1 -0
  187. package/dist/sensor/eqe-electron-rate.js +1 -7
  188. package/dist/sensor/eqe-electron-rate.js.map +1 -1
  189. package/dist/sensor/exposure-window.d.ts +2 -0
  190. package/dist/sensor/exposure-window.js +2 -2
  191. package/dist/sensor/exposure-window.js.map +1 -1
  192. package/dist/sensor/readout-timing.js +2 -23
  193. package/dist/sensor/readout-timing.js.map +1 -1
  194. package/dist/sensor/response-application-compatibility.js +1 -6
  195. package/dist/sensor/response-application-compatibility.js.map +1 -1
  196. package/dist/sensor/responsivity-photocurrent.js +1 -7
  197. package/dist/sensor/responsivity-photocurrent.js.map +1 -1
  198. package/dist/sensor/sensor-characteristics.d.ts +147 -0
  199. package/dist/sensor/sensor-characteristics.js +126 -0
  200. package/dist/sensor/sensor-characteristics.js.map +1 -0
  201. package/dist/sensor/spatial-sample-reduction.js +1 -5
  202. package/dist/sensor/spatial-sample-reduction.js.map +1 -1
  203. package/dist/sensor/spectral-quadrature.js +1 -6
  204. package/dist/sensor/spectral-quadrature.js.map +1 -1
  205. package/dist/sensor/temporal-photo-signal.d.ts +3 -0
  206. package/dist/sensor/temporal-photo-signal.js +5 -2
  207. package/dist/sensor/temporal-photo-signal.js.map +1 -1
  208. package/dist/stabilization/system.d.ts +9 -0
  209. package/dist/stabilization/system.js +5 -6
  210. package/dist/stabilization/system.js.map +1 -1
  211. package/docs/AGENT_GUIDE.md +5 -0
  212. package/docs/API_REFERENCE.md +173 -2
  213. package/docs/QUICK_START.md +213 -6
  214. package/docs/USAGE.md +157 -3
  215. package/docs/api/capture-native-raw-dng.md +6 -0
  216. package/docs/api/core-version.md +1 -1
  217. package/docs/api/equipment-generic-lens-catalogue.md +17 -7
  218. package/docs/api/equipment-sensor-profile-body.md +64 -0
  219. package/docs/api/equipment-sensor-profiles.md +86 -0
  220. package/docs/api/exports.json +357 -2
  221. package/docs/api/facade-display-view-gpu.md +5 -2
  222. package/docs/api/facade-exposure-build-up.md +105 -0
  223. package/docs/api/facade-fast-methods.md +99 -0
  224. package/docs/api/facade-image-plane-enable.md +16 -0
  225. package/docs/api/facade-mixed-illumination.md +67 -0
  226. package/docs/api/facade-representative-light-sources.md +131 -0
  227. package/docs/api/facade-scene-radiance.md +54 -3
  228. package/docs/api/facade-simulate-capture.md +60 -15
  229. package/docs/api/facade-simulate-display-view.md +3 -2
  230. package/docs/api/motion-subject-motion.md +23 -0
  231. package/docs/api/optics-aperture-diameter.md +51 -0
  232. package/docs/api/optics-detail-limit.md +203 -0
  233. package/docs/api/optics-generic-lens-lateral-color.md +79 -0
  234. package/docs/api/output-crop-to-match-focal-length.md +67 -0
  235. package/docs/api/output-developed-lens-corrections.md +10 -1
  236. package/docs/api/output-generic-lens-corrections.md +40 -2
  237. package/docs/api/output-lens-corrections.md +11 -1
  238. package/docs/api/output-print-plan.md +35 -1
  239. package/docs/api/output-print-size-blur-noise.md +115 -0
  240. package/docs/api/sensor-sensor-characteristics.md +105 -0
  241. package/llms.txt +6 -0
  242. package/package.json +1 -1
package/docs/USAGE.md CHANGED
@@ -7146,7 +7146,9 @@ console.log(png.length, same);
7146
7146
  See [Print planning](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md) for a complete, strict packed example and the exact-ratio integer policy.
7147
7147
 
7148
7148
  - `calculatePrintPlan()` says whether the native sampling is enough, and gives a compatible minimum file raster.
7149
+ - `fit: { kind: "contain", paperOrientation }` (2.22.0) fits the whole image inside paper of another shape and reports the borders ([Contain fit](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md#integer-policy-fit-and-sufficiency)).
7149
7150
  - `calculatePrintSizeLimit()` distinguishes finite, unbounded and unavailable sampling limits.
7151
+ - `calculatePrintSizeLimitWithBlurAndNoise()` (2.20.0) gives the largest print at a viewing distance that still looks sharp and clean, counting blur and noise as well as pixels, and names the limiting factor ([Print size limit with blur and noise](https://github.com/Photivra/photivra/blob/main/docs/PRINT_PLANNING.md#print-size-limit-with-blur-and-noise)). An approximation.
7150
7152
  - Print quality and delivery are not assessed.
7151
7153
 
7152
7154
  The [regional Print detail diagnostic](https://github.com/Photivra/photivra/blob/main/docs/PRINT_DETAIL_ASSESSMENT.md) measures one declared, coherent sinusoidal fundamental in a selected linear-luminance ROI, and records the exact pipeline stage it measured. Its [validation protocol](https://github.com/Photivra/photivra/blob/main/docs/PRINT_DETAIL_VALIDATION.md) and [capability ledger](https://github.com/Photivra/photivra/blob/main/docs/validation/print-detail-capabilities.json) separate mathematical conformance from the still-blocked qualification of acquired images, renderers and perceived quality.
@@ -7511,17 +7513,41 @@ console.log(calculateGeometricFocusBreathing({
7511
7513
  }).value.deltaDegrees);
7512
7514
  ```
7513
7515
 
7516
+ ## Sensor profiles (#554)
7517
+
7518
+ `SENSOR_PROFILES` lists eight representative sensors (full frame, APS-C, medium format and a large phone sensor; BSI,
7519
+ stacked and front-illuminated; 2010s and 2020s) whose values come from published specifications, values derived from them
7520
+ or labelled design values, with no maker or product named. `simulateCapture({ sensor: { profile: record } })` simulates one
7521
+ (the record is checked strictly; `resolveSensorProfile(id)` finds one by id):
7522
+ it replaces the body's ISO range, readout regimes, readout time and fixed-pattern noise, and the #555 characteristics
7523
+ override it one at a time. `resolveSensorProfile(id)` returns one profile; `SENSOR_PROFILE_ISO_ANCHOR` is the ISO anchor its base ISO is re-rated from. Values, sources and the ISO
7524
+ rating: [SENSOR_PROFILES.md](https://github.com/Photivra/photivra/blob/main/docs/SENSOR_PROFILES.md).
7525
+
7526
+ ```ts
7527
+ import { SENSOR_PROFILES, simulateCapture } from "@photivra/engine";
7528
+
7529
+ for (const p of SENSOR_PROFILES) {
7530
+ // fast: false (full quality, as the calculator asks) computes the mid-gray noise figure.
7531
+ const r = await simulateCapture({ scene: { preset: "uniform" }, fast: false, sensor: { profile: p }, exposure: { mode: "manual", iso: 3200, shutterSeconds: 1 / 4000 } });
7532
+ console.log(p.name, r.sensor.dynamicRangeStops.toFixed(1), r.sensor.midGrey?.snr.toFixed(1));
7533
+ }
7534
+ ```
7535
+
7514
7536
  ## Generic lens catalogue (#549)
7515
7537
 
7516
7538
  `GENERIC_LENS_CATALOGUE` lists the engine's 30 generic primes (17 professional, 13 consumer, "PV 50 1.2 Pro", "PV 50 1.8"; Photivra's
7517
7539
  own designs, not real products). `resolveGenericLens` resolves one for a sensor, an f-number and a focus distance: its focal length
7518
7540
  (scaled by the crop factor), aperture range, close focus, projection and its character, Seidel aberrations, axial and lateral
7519
- colour, distortion and vignetting profiles, flare, breathing and focus scatter, each a labelled approximation.
7541
+ colour, distortion and vignetting profiles, flare, breathing, focus scatter and its bokeh (`character.bokeh`, #567: the options of
7542
+ `lens.defocus` and `calculateDefocusBokehKernel`), each a labelled approximation.
7520
7543
  `simulateGenericLensFocusError` draws a seeded autofocus error; `createGenericLensCorrectionProfile` builds the lens's generic
7521
7544
  correction profile for `resolveLensCorrectionPlan`, which `simulateCapture` applies to its developed image with
7522
- `output.lensCorrections` (#570; never to the RAW or the DNG). `simulateCapture` renders a catalogue lens with `lens.catalogue`
7545
+ `output.lensCorrections` (#570; never to the RAW or the DNG), and which a DNG can carry for RAW editors as optional DNG opcodes with
7546
+ `output.dng.lensCorrectionOpcodes` (#587; the RAW codes unchanged). `simulateCapture` renders a catalogue lens with `lens.catalogue`
7523
7547
  ([quick start](QUICK_START.md#generic-lens-catalogue-549)); `fast: true` (or `lens: { catalogue: { kind, fast: true } }`) takes its
7524
- fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures. Equations, tables and the
7548
+ fast method (#568), a labeled approximation that blends the PSF over a field grid, for interactive captures.
7549
+ `calculateLateralColorFringe({ lensId, activeImagingArea, pixelPitchMicrometers, fieldPosition? })` gives the lens's lateral color as a
7550
+ fringe width: the blue-minus-red image displacement in µm and pixels at a field position (default the corner; #580). Equations, tables and the
7525
7551
  chart measurements: [LENS_CATALOGUE.md](https://github.com/Photivra/photivra/blob/main/docs/LENS_CATALOGUE.md).
7526
7552
 
7527
7553
  ```ts
@@ -7787,6 +7813,13 @@ including the whole-image stages `output.diffractionCorrection` and `output.shar
7787
7813
  bytes as a whole-frame `simulateCapture()` and per-row progress (`diffraction-correction`, `sharpening`). See
7788
7814
  [Quick start, Developing a capture rendered in parts](QUICK_START.md#developing-a-capture-rendered-in-parts-571).
7789
7815
 
7816
+ `simulateExposureBuildUp(options, { fractions })` (#534) renders fractions of one exposure as one accumulating process. Each
7817
+ fraction's expected signal is its own capture at the elapsed time, so camera rotation, a moving subject and mixed lighting are
7818
+ included. Its photo and dark counts are binomial splits of the next fraction's, so they build up in place. Each fraction is
7819
+ clipped at the full well, read out once with its own read noise and developed as its own photo. The fraction at 1 is
7820
+ `simulateCapture(options)` byte for byte. Only the sRGB sample formats are accepted (on screen). See
7821
+ [Quick start, Exposure build-up](QUICK_START.md#exposure-build-up-534).
7822
+
7790
7823
  ### Color charts (2.14)
7791
7824
 
7792
7825
  The engine's own [color patches](https://github.com/Photivra/photivra/blob/main/docs/COLOR_PATCHES.md) are reflectance spectra lit by the capture's illuminant. Draw the
@@ -7807,6 +7840,33 @@ const sky = getColorPatchSet().patches.find((p) => p.patchId === "sky-blue");
7807
7840
  console.log(tungsten.scene.parameters.patchSet, sky?.code, sky?.name, sky?.reference.cielabD65); // "PV-M1", "Midday Sky"
7808
7841
  ```
7809
7842
 
7843
+ ### A photo as a print (2.21)
7844
+
7845
+ An [image-plane scene](https://github.com/Photivra/photivra/blob/main/docs/IMAGE_PLANE.md) (#581) puts an sRGB (8- or 16-bit) or linear image in front of the camera as a flat matte
7846
+ print. Each pixel becomes a smooth reflectance spectrum in [0, 1] that gives back its sRGB under CIE D65. Under other lights the print's colors are an approximation.
7847
+ `chartSize` sets the print's size (its height follows the image). Without it the print is fitted inside the frame. `prepareDisplayView` renders it on WebGPU in the Speed path.
7848
+ `prepareImagePlaneScenes()` loads the scene's module first, so bundles that never print a photo do not carry it.
7849
+
7850
+ ```ts
7851
+ import { prepareImagePlaneScenes, simulateCapture } from "@photivra/engine";
7852
+
7853
+ await prepareImagePlaneScenes(); // loads the scene's module (a lazy chunk) once
7854
+ const width = 2, height = 1, data = Uint8Array.of(255, 255, 255, 30, 60, 200);
7855
+ const print = await simulateCapture({
7856
+ scene: { kind: "image-plane", sceneId: "snapshot", image: { width, height, channels: 3, encoding: "srgb-8", data }, paper: { whiteReflectance: 0.9 } },
7857
+ chartSize: { widthM: 0.1 },
7858
+ subjectDistanceM: 0.4,
7859
+ output: { width: 32, height: 16 }
7860
+ });
7861
+ console.log(print.scene.parameters.encoding, print.assumptions.some((a) => a.id === "scene.imagePlane")); // "srgb-8", true
7862
+ ```
7863
+
7864
+ **Representative light sources (2.22).** The engine's own [representative light sources](https://github.com/Photivra/photivra/blob/main/docs/LIGHT_SOURCES.md) are four relative spectra
7865
+ designed by Photivra: a tri-phosphor fluorescent tube and white LEDs of low and high color rendering at 4000 K and a warm one at
7866
+ 2700 K, each "representative, not a named product". `createRepresentativeLightSourceIlluminant({ sourceId })` returns an
7867
+ `illuminant` (`{ kind: "spectrum", spectrum, label }`); under the two 4000 K LEDs the color patches differ by up to ΔE00 5.9 while
7868
+ the neutrals stay neutral. Use `sensor.spectralStepNanometers: 5`. The page has a runnable example.
7869
+
7810
7870
  In `aperture-priority` mode, the shutter comes from the meter (CIPA DC-004 relations S = 10/H and H = 0.65·L·t/N²) through
7811
7871
  `resolveAperturePriorityExposureMode()`.
7812
7872
  `exposure.standardExposureDeviationStops` is the exposure used minus the standard exposure for the metered luminance L, in stops,
@@ -7863,6 +7923,28 @@ The chart presets are also available on their own:
7863
7923
  - `createPlanarSceneRadianceProvider()` turns a chart into an `evaluateRadiance` callback for the native
7864
7924
  environment RAW contract.
7865
7925
 
7926
+ ### Mixed lighting (2.22, #550)
7927
+
7928
+ `illumination` lights the chart with two or more sources blended across it (a linear gradient, a radial falloff or regions),
7929
+ for example window daylight on one side and a lamp on the other. The frame center's light sets the grey-card meter and its
7930
+ strongest source the `"scene-illuminant"` white balance; both are reported in `assumptions`. See
7931
+ [Mixed lighting](https://github.com/Photivra/photivra/blob/main/docs/MIXED_ILLUMINATION.md) for the model and every field.
7932
+
7933
+ ```ts
7934
+ import { simulateCapture } from "@photivra/engine";
7935
+
7936
+ const lampPool = await simulateCapture({
7937
+ scene: { preset: "step-wedge" },
7938
+ exposure: { mode: "aperture-priority", metering: "grey-card" },
7939
+ illumination: {
7940
+ sources: [{ illuminant: "cie-a", illuminanceLux: 500 }, { illuminant: { kind: "planckian", temperatureKelvin: 6500 }, illuminanceLux: 20000 }],
7941
+ blend: { kind: "radial", radius: 0.8, stops: [0.25, 1] }
7942
+ },
7943
+ output: { width: 32, height: 16 }
7944
+ });
7945
+ console.log(lampPool.assumptions.find((a) => a.id === "output.whiteBalanceSource")?.value); // { source: 0, illuminant: "cie-a" }
7946
+ ```
7947
+
7866
7948
  ### Display view of a capture
7867
7949
 
7868
7950
  `simulateDisplayView(options, view)` takes a capture request and a display size and returns the real
@@ -8048,6 +8130,18 @@ const rolling = await simulateCapture({ scene: { preset: "checkerboard" }, motio
8048
8130
  console.log(rolling.request.siteInput(7 * 8).charge.completenessProfile.startOffsetSecondsFromOpeningReference);
8049
8131
  ```
8050
8132
 
8133
+ `motion.subject` (2.22.0) moves the subject instead: the whole scene, or one `region` of it over a still background,
8134
+ at `velocityMPerSec` on the scene plane, optionally rotating at `angularVelocityRadPerSec` about the optical axis. It
8135
+ uses the same temporal midpoints, so a rolling shutter skews the moving subject. The display view renders sideways
8136
+ motion under a global shutter and refuses the rest. See [Subject motion](QUICK_START.md#subject-motion).
8137
+
8138
+ ```ts
8139
+ import { simulateCapture } from "@photivra/engine";
8140
+
8141
+ const moving = await simulateCapture({ scene: { preset: "slanted-edge" }, motion: { subject: { velocityMPerSec: { x: 0.5 } } }, output: { width: 8, height: 8 } });
8142
+ console.log(moving.assumptions.find((a) => a.id === "motion.subject")?.value);
8143
+ ```
8144
+
8051
8145
  ## Equivalent f-number, sensor area and blur as a share of the frame
8052
8146
 
8053
8147
  `calculateSensorEquivalence()` says what a lens on one sensor corresponds to on a 36 × 24 mm frame (or on a `reference` you pass). Call it once per sensor to fill a lens table:
@@ -8085,3 +8179,63 @@ console.log(share.print?.blurOnPrintMm); // 0.25 mm on the print
8085
8179
  - Equal shares of the frame diagonal look the same when images are shown at the same size. `calculateBlurShareOfFrame()` does not judge whether a blur is visible.
8086
8180
 
8087
8181
  See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#sensor-equivalence-f-number-area-and-blur-share).
8182
+
8183
+ ## Aperture diameter and cropping to match a longer lens
8184
+
8185
+ `calculateApertureDiameter()` gives the entrance-pupil diameter in millimetres, focal length ÷ f-number (exact by the definition of the f-number, ideal lens focused at infinity). `calculateCropToMatchFocalLength()` gives the centred crop that makes an image from a shorter lens match a longer lens's framing from the same position, and the pixels it leaves:
8186
+
8187
+ ```ts
8188
+ import { calculateApertureDiameter, calculateCropToMatchFocalLength } from "@photivra/engine";
8189
+
8190
+ console.log(calculateApertureDiameter({ focalLengthMm: 50, fNumber: 2 }).value.entrancePupilDiameterMm); // 25
8191
+ console.log(calculateApertureDiameter({ focalLengthMm: 50, fNumber: 4 }).value.entrancePupilDiameterMm); // 12.5
8192
+
8193
+ const crop = calculateCropToMatchFocalLength({
8194
+ focalLengthMm: 50,
8195
+ targetFocalLengthMm: 100,
8196
+ pixelWidth: 6000,
8197
+ pixelHeight: 4000
8198
+ }).value;
8199
+
8200
+ console.log(crop.cropFactor); // 2
8201
+ console.log(crop.pixelWidth, crop.pixelHeight); // 3000 2000
8202
+ console.log(crop.megapixels, crop.sourceMegapixels); // 6 24
8203
+ ```
8204
+
8205
+ - The aperture diameter is the entrance pupil (the aperture stop seen from the front of the lens), not the diaphragm's opening. Close focus (working f-number `N·(1 + m/P)`, pupil magnification `P`) is not modelled.
8206
+ - The crop factor `target ÷ focal length` is exact for ideal rectilinear lenses focused at infinity; at close focus the matching factor is larger, `F·(d − f)/(f·(d − F))` at object distance `d` (thin lens), which is not applied. The crop is `calculateCenteredCrop()`'s (aspect ratio kept to whole pixels), and `targetFocalLengthMm` must be at least `focalLengthMm`.
8207
+
8208
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#aperture-entrance-pupil-diameter).
8209
+
8210
+ ## Which limits detail: the lens or the sensor
8211
+
8212
+ `calculateDetailLimit()` (model `detail-limit` 1.0.0, #575) answers "would a sharper lens or a denser sensor help?" for given settings. It returns the optics' resolution (MTF50 in cycles/mm, from diffraction, the lens's aberrations and any defocus), the sampling's (the Nyquist frequency 1/(2p) and, with a Bayer filter, the red and blue sites' 1/(4p)), the system MTF50, and which one limits:
8213
+
8214
+ ```ts
8215
+ import { calculateDetailLimit } from "@photivra/engine";
8216
+
8217
+ const sensor = { widthMm: 36, heightMm: 24, pixelWidth: 6000 }; // 6 µm pixels
8218
+
8219
+ const stoppedDown = calculateDetailLimit({ sensor, lens: { kind: "ideal", focalLengthMm: 50 }, fNumber: 22 }).value;
8220
+ console.log(stoppedDown.limitedBy, stoppedDown.limitingFactor); // "lens" "diffraction"
8221
+ console.log(stoppedDown.optics.mtf50CyclesPerMm); // ≈ 33.4, against a Nyquist frequency of ≈ 83.3
8222
+ console.log(stoppedDown.diffractionLimitedFNumber); // ≈ 8.8: from here on diffraction limits on this sensor
8223
+
8224
+ const wideOpen = calculateDetailLimit({ sensor, lens: { kind: "catalogue", lensId: "pv-50-1.2-pro" }, fNumber: 4 }).value;
8225
+ console.log(wideOpen.limitedBy, wideOpen.limitingFactor); // "sensor" "pixel-pitch"
8226
+
8227
+ const outOfFocus = calculateDetailLimit({
8228
+ sensor,
8229
+ lens: { kind: "ideal", focalLengthMm: 50 },
8230
+ fNumber: 2,
8231
+ focus: { focusDistanceM: 3, subjectDistanceM: 2.5 }
8232
+ }).value;
8233
+ console.log(outOfFocus.limitingFactor); // "defocus"
8234
+ ```
8235
+
8236
+ - The sensor limits when the optics still pass 50 % contrast at the Nyquist frequency (`opticsToNyquistRatio ≥ 1`): a denser sensor would record finer detail. Otherwise the lens limits, through `"diffraction"` when its RMS wavefront error is within λ/14 (the Maréchal criterion), else `"aberrations"` or `"defocus"`, whichever is larger. This rule is an approximation; the MTF values are the exact scalar OTF of the stated wavefront at one wavelength (550 nm unless `wavelengthNm` is given).
8237
+ - `lens.catalogue` lenses use their generic Seidel aberrations at `fieldPosition` (0 the center, 1 the corner), so corners are softer; `lens.ideal` has none.
8238
+ - Tangential and sagittal MTF50 are both reported; the decision uses the lower one.
8239
+ - Demosaicing, sharpening, noise and motion are not included.
8240
+
8241
+ See [Physics Foundation](https://github.com/Photivra/photivra/blob/main/docs/PHYSICS_FOUNDATION.md#which-limits-detail-the-lens-or-the-sensor).
@@ -82,5 +82,11 @@ export interface SerializeNativeRawDngInput {
82
82
  capturedAtUtc?: string;
83
83
  /** Optional (2.4) caller text: description, software, copyright, comment (see the module notes). If omitted, no text is added. */
84
84
  textMetadata?: OutputTextMetadata;
85
+ /**
86
+ * Optional (#587) OpcodeList3 (tag 51022) bytes, for example `createDngLensCorrectionOpcodes(...).opcodeList3`: a big-endian DNG
87
+ * opcode list (DNG 1.3.0.0+, "Opcode List Processing") that readers apply to the demosaiced stage-3 image. Only its framing is
88
+ * checked (count, per-opcode headers and parameter sizes filling the bytes exactly). The RAW codes are unchanged. If omitted, no tag.
89
+ */
90
+ opcodeList3?: Uint8Array;
85
91
  }
86
92
  ```
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
11
11
  own versions. Release checks confirm it matches package.json and the packed package.
12
12
 
13
13
  ```ts
14
- ENGINE_API_VERSION = "2.19.0" as const
14
+ ENGINE_API_VERSION = "2.21.0" as const
15
15
  ```
@@ -4,11 +4,11 @@ The package and root API versions are recorded once in [Navigation](../API_REFER
4
4
 
5
5
  ## GENERIC_LENS_CATALOGUE
6
6
 
7
- The 30 lenses: 17 professional, then 13 consumer, each set by focal length.
7
+ The 33 lenses: 17 professional, 13 consumer and (#554) 3 phone lenses, each set by focal length.
8
8
 
9
9
  ```ts
10
10
  GENERIC_LENS_CATALOGUE: readonly GenericLensCatalogueEntry[] = Object.freeze([...PRO.map((row) => entry("professional", row)),
11
- ...CONSUMER.map((row) => entry("consumer", row))])
11
+ ...CONSUMER.map((row) => entry("consumer", row)), ...PHONE.map((row) => entry("phone", row))])
12
12
  ```
13
13
 
14
14
  ## GENERIC_LENS_IDS
@@ -30,7 +30,12 @@ GENERIC_LENS_SET_BASELINES: Readonly<Record<GenericLensSet, Readonly<GenericLens
30
30
  breathingFraction: -0.03, focusScatterDepthOfFocusFraction: 0.1, closeFocusFactor: 0.15 }),
31
31
  consumer: Object.freeze({ sphericalWaves: 0.5, comaWaves: 1.0, astigmatismWaves: 0.8, fieldCurvatureWaves: 0.45, axialColorWaves: 0.4,
32
32
  lateralColorFraction: 0.0003, distortionMagnitude: 0.03, naturalVignettingStops: 0.7, opticalVignettingStops: 1.1, veilingGlareFraction: 0.02, ghostFraction: 0.008,
33
- breathingFraction: -0.08, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 })
33
+ breathingFraction: -0.08, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 }),
34
+ // #554: design values (docs/LENS_CATALOGUE.md, "Phone lenses"): the consumer baseline with the stronger fall-off, flare and distortion
35
+ // (corrected in software on phones) of small fixed-aperture phone lenses, and no optical-vignetting change (the aperture is fixed).
36
+ phone: Object.freeze({ sphericalWaves: 0.5, comaWaves: 1.0, astigmatismWaves: 0.8, fieldCurvatureWaves: 0.5, axialColorWaves: 0.3,
37
+ lateralColorFraction: 0.0004, distortionMagnitude: 0.03, naturalVignettingStops: 1.2, opticalVignettingStops: 0.5, veilingGlareFraction: 0.03, ghostFraction: 0.01,
38
+ breathingFraction: -0.05, focusScatterDepthOfFocusFraction: 0.3, closeFocusFactor: 0.5 })
34
39
  })
35
40
  ```
36
41
 
@@ -69,7 +74,7 @@ export interface GenericLensCatalogueEntry {
69
74
  note: string;
70
75
  /**
71
76
  * A short plain-English summary of the lens's character (at most 40 characters, US spelling), written from its adjustments and
72
- * `note`, for lists and pickers. It describes only what the catalogue models (bokeh shape is not modeled: the defocus is a disc).
77
+ * `note`, for lists and pickers. It describes the lens's sharpness, color, distortion and falloff, not its bokeh (the same rule for every lens of a set).
73
78
  */
74
79
  summary: string;
75
80
  }
@@ -102,6 +107,11 @@ export interface GenericLensCharacter {
102
107
  breathing: { fieldOfViewChange: number; atMinimumFocus: number };
103
108
  /** Standard deviation of the autofocus error as a fraction of the depth of focus 2·N·c. */
104
109
  focusScatterDepthOfFocusFraction: number;
110
+ /**
111
+ * Bokeh character (#567) at this f-number, focus and sensor, as the options of `lens.defocus` and `calculateDefocusBokehKernel`
112
+ * (#553): blades, the resolved W040 as `sphericalAberration` (µm at this f-number) and, for fast wides and normals, `catsEye`.
113
+ */
114
+ bokeh: DefocusBokehOptions;
105
115
  }
106
116
  ```
107
117
 
@@ -127,7 +137,7 @@ export interface GenericLensCharacterValues {
127
137
  distortionMagnitude: number;
128
138
  /** Corner fall-off in stops that stays when stopped down (natural vignetting). */
129
139
  naturalVignettingStops: number;
130
- /** Extra corner fall-off in stops at the widest aperture (optical vignetting); gone two stops down. */
140
+ /** Extra corner fall-off in stops at the widest aperture (optical vignetting); gone four stops down (N = 4·N0). */
131
141
  opticalVignettingStops: number;
132
142
  /** Veiling glare: the fraction of the image's mean irradiance spread as a uniform veil. */
133
143
  veilingGlareFraction: number;
@@ -174,10 +184,10 @@ export type GenericLensKind = "standard" | "macro" | "fisheye";
174
184
 
175
185
  ## GenericLensSet
176
186
 
177
- The exact contract is declared below; use the domain guide for assumptions and staged integration.
187
+ #554: "phone", the three fixed lenses of a phone sensor profile (`sensor.profile` with `fixedLenses: "phone"`).
178
188
 
179
189
  ```ts
180
- export type GenericLensSet = "professional" | "consumer";
190
+ export type GenericLensSet = "professional" | "consumer" | "phone";
181
191
  ```
182
192
 
183
193
  ## ResolvedGenericLens
@@ -0,0 +1,64 @@
1
+ # equipment/sensor-profile-body.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## SENSOR_PROFILE_ISO_ANCHOR
6
+
7
+ The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5.
8
+
9
+ ```ts
10
+ SENSOR_PROFILE_ISO_ANCHOR = Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 })
11
+ ```
12
+
13
+ ## SensorProfile
14
+
15
+ One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind.
16
+
17
+ ```ts
18
+ export interface SensorProfile {
19
+ id: string;
20
+ /** Format, resolution, architecture and era; no maker or product name. */
21
+ name: string;
22
+ /** A market category, not a specification: it is a label and changes nothing. */
23
+ marketTag: "consumer" | "professional";
24
+ architecture: "front-illuminated" | "back-illuminated" | "stacked";
25
+ sensorWidthMm: number; sensorHeightMm: number;
26
+ pixelsWide: number; pixelsHigh: number;
27
+ pixelPitchMicrometers: number;
28
+ /** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */
29
+ peakQuantumEfficiency: number;
30
+ /** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */
31
+ fullWellElectrons: number;
32
+ /** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */
33
+ readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };
34
+ /** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */
35
+ highGainFullWellElectrons: number | null;
36
+ adcBitDepth: 12 | 14 | 16;
37
+ /** Re-rated base ISO (see the module comment) and the highest ISO setting. */
38
+ baseIso: number; maximumIso: number;
39
+ /** Largest analog gain relative to base ISO; above it the gain is digital. */
40
+ maximumAnalogGain: number;
41
+ /** Rolling readout, s: the top-to-bottom skew with `motion.shutter: "rolling"`. */
42
+ readoutSeconds: number;
43
+ /** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */
44
+ fixedPattern: { prnu: number; dsnu: number; hotPixelFraction: number; hotPixelMultiplier: number; rowElectrons: number; columnElectrons: number };
45
+ /** What the profile is good for (text only). */
46
+ goodFor: readonly string[];
47
+ description: string;
48
+ /** "phone": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */
49
+ fixedLenses: "phone" | null;
50
+ /**
51
+ * Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md "Sources"); a value not listed is a design value.
52
+ * For example `fullWellElectrons: "published S1"` or `baseIso: "derived"`.
53
+ */
54
+ provenance: Readonly<Partial<Record<keyof SensorProfile | "sensorSize" | "pixels" | "highGainFromIso", string>>>;
55
+ }
56
+ ```
57
+
58
+ ## SensorProfileValueKind
59
+
60
+ How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value.
61
+
62
+ ```ts
63
+ export type SensorProfileValueKind = "published" | "derived" | "design";
64
+ ```
@@ -0,0 +1,86 @@
1
+ # equipment/sensor-profiles.ts public contracts
2
+
3
+ The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
4
+
5
+ ## resolveSensorProfile
6
+
7
+ The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise.
8
+
9
+ ```ts
10
+ export function resolveSensorProfile(id: unknown): SensorProfile;
11
+ ```
12
+
13
+ This call is synchronous; any supplied provider must follow its explicit synchronous contract.
14
+
15
+ ## SENSOR_PROFILE_IDS
16
+
17
+ The profile ids, in catalogue order.
18
+
19
+ ```ts
20
+ SENSOR_PROFILE_IDS: readonly string[] = Object.freeze(SENSOR_PROFILES.map((p) => p.id))
21
+ ```
22
+
23
+ ## SENSOR_PROFILES
24
+
25
+ The catalogue, in the issue's order. Frozen.
26
+
27
+ ```ts
28
+ SENSOR_PROFILES: readonly SensorProfile[] = Object.freeze(([
29
+ { id: "full-frame-24mp-bsi-2020s", name: "Full frame 24 MP BSI, 2020s", marketTag: "consumer", architecture: "back-illuminated",
30
+ sensorWidthMm: 36.07, sensorHeightMm: 24.01, pixelsWide: 6072, pixelsHigh: 4042, pixelPitchMicrometers: 5.94, peakQuantumEfficiency: .8,
31
+ fullWellElectrons: 100000, readNoise: { lowGainElectrons: 6.4, highGainElectrons: 1.1, highGainFromIso: 500 }, highGainFullWellElectrons: 20000,
32
+ adcBitDepth: 14, baseIso: 100, maximumIso: 51200, maximumAnalogGain: 64, readoutSeconds: .05, fixedPattern: BSI,
33
+ goodFor: ["landscape", "portrait", "travel"], description: "Large pixels and a deep full well for wide dynamic range at base ISO; a moderate readout shows some rolling-shutter skew.",
34
+ fixedLenses: null, provenance: { ...sourced("S1", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise", "adcBitDepth"),
35
+ sensorSize: "derived", architecture: "published S1", highGainFromIso: "derived S1", highGainFullWellElectrons: "derived", baseIso: "derived" } },
36
+ { id: "full-frame-45mp-stacked-2020s", name: "Full frame 45 MP stacked, 2020s", marketTag: "professional", architecture: "stacked",
37
+ sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 8256, pixelsHigh: 5504, pixelPitchMicrometers: 4.36, peakQuantumEfficiency: .8,
38
+ fullWellElectrons: 69000, readNoise: { lowGainElectrons: 3.5, highGainElectrons: 1.5, highGainFromIso: 250 }, highGainFullWellElectrons: 22080,
39
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .005, fixedPattern: BSI,
40
+ goodFor: ["landscape", "studio", "wildlife"], description: "High resolution with a fast stacked readout: fine detail for landscape and studio work, little rolling-shutter skew for action.",
41
+ fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", fullWellElectrons: "design (S2 scaled by pixel area)", readNoise: "design (S2)", highGainFromIso: "design (S2)",
42
+ highGainFullWellElectrons: "derived", baseIso: "derived" } },
43
+ { id: "full-frame-24mp-stacked-sports-2020s", name: "Full frame 24 MP stacked (sports), 2020s", marketTag: "professional", architecture: "stacked",
44
+ sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 6000, pixelsHigh: 4000, pixelPitchMicrometers: 6, peakQuantumEfficiency: .8,
45
+ fullWellElectrons: 80000, readNoise: { lowGainElectrons: 3, highGainElectrons: 1, highGainFromIso: 400 }, highGainFullWellElectrons: 25000,
46
+ adcBitDepth: 14, baseIso: 125, maximumIso: 102400, maximumAnalogGain: 64, readoutSeconds: .0025, fixedPattern: BSI,
47
+ goodFor: ["sports", "wildlife", "journalism"], description: "The fastest readout and low high-gain read noise: almost no rolling-shutter skew and clean high ISO, at a moderate resolution.",
48
+ fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", highGainFullWellElectrons: "derived", baseIso: "derived" } },
49
+ { id: "full-frame-24mp-fsi-2010s", name: "Full frame 24 MP front-illuminated, 2010s", marketTag: "professional", architecture: "front-illuminated",
50
+ sensorWidthMm: 36.01, sensorHeightMm: 24.07, pixelsWide: 6032, pixelsHigh: 4032, pixelPitchMicrometers: 5.97, peakQuantumEfficiency: .53,
51
+ fullWellElectrons: 76000, readNoise: { lowGainElectrons: 4, highGainElectrons: 4, highGainFromIso: null }, highGainFullWellElectrons: null,
52
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .08, fixedPattern: FSI,
53
+ goodFor: ["portrait", "landscape", "journalism"], description: "An older professional sensor: lower QE and one conversion gain, so a modern consumer BSI sensor beats it at high ISO; slow readout.",
54
+ fixedLenses: null, provenance: { ...sourced("S3", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "adcBitDepth"),
55
+ sensorSize: "derived", architecture: "derived S4", baseIso: "derived" } },
56
+ { id: "aps-c-24mp-fsi-2020s", name: "APS-C 24 MP front-illuminated, 2020s", marketTag: "consumer", architecture: "front-illuminated",
57
+ sensorWidthMm: 23.55, sensorHeightMm: 15.73, pixelsWide: 6024, pixelsHigh: 4024, pixelPitchMicrometers: 3.91, peakQuantumEfficiency: .55,
58
+ fullWellElectrons: 36000, readNoise: { lowGainElectrons: 2.7, highGainElectrons: 2.7, highGainFromIso: null }, highGainFullWellElectrons: null,
59
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .06, fixedPattern: FSI,
60
+ goodFor: ["travel", "family", "learning"], description: "A smaller, front-illuminated sensor: less light per pixel, a slower readout and more rolling-shutter skew.",
61
+ fixedLenses: null, provenance: { ...sourced("S5", "pixels", "pixelPitchMicrometers", "architecture", "fullWellElectrons", "readNoise", "adcBitDepth"), sensorSize: "derived", baseIso: "derived" } },
62
+ { id: "aps-c-26mp-bsi-2020s", name: "APS-C 26 MP BSI, 2020s", marketTag: "professional", architecture: "back-illuminated",
63
+ sensorWidthMm: 23.49, sensorHeightMm: 15.7, pixelsWide: 6248, pixelsHigh: 4176, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
64
+ fullWellElectrons: 50000, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16000,
65
+ adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .025, fixedPattern: BSI,
66
+ goodFor: ["wildlife", "sports", "travel"], description: "A BSI APS-C sensor with dual conversion gain whose readout beats the consumer full-frame profile's.",
67
+ fixedLenses: null, provenance: { ...sourced("S6", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise"), sensorSize: "derived",
68
+ architecture: "published S6", highGainFromIso: "design (S7)", highGainFullWellElectrons: "derived", baseIso: "derived" } },
69
+ { id: "medium-format-100mp-bsi-16bit", name: "Medium format 100 MP BSI, 16-bit", marketTag: "professional", architecture: "back-illuminated",
70
+ sensorWidthMm: 43.856, sensorHeightMm: 32.9, pixelsWide: 11656, pixelsHigh: 8750, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
71
+ fullWellElectrons: 50300, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16096,
72
+ adcBitDepth: 16, baseIso: 80, maximumIso: 12800, maximumAnalogGain: 64, readoutSeconds: .25, fixedPattern: BSI,
73
+ goodFor: ["studio", "landscape", "portrait"], description: "A large sensor with 100 MP and a 16-bit RAW; a slow readout makes its electronic shutter unsuited to motion.",
74
+ fixedLenses: null, provenance: { ...sourced("S7", "pixels", "pixelPitchMicrometers", "sensorSize", "fullWellElectrons", "readNoise", "adcBitDepth"), architecture: "published S8",
75
+ highGainFromIso: "derived S7", highGainFullWellElectrons: "derived", baseIso: "derived" } },
76
+ { id: "phone-50mp-stacked", name: "Phone, large, 50 MP stacked", marketTag: "consumer", architecture: "stacked",
77
+ sensorWidthMm: 9.844, sensorHeightMm: 7.431, pixelsWide: 8192, pixelsHigh: 6144, pixelPitchMicrometers: 1.197, peakQuantumEfficiency: .75,
78
+ fullWellElectrons: 8600, readNoise: { lowGainElectrons: 3, highGainElectrons: 1.4, highGainFromIso: 160 }, highGainFullWellElectrons: 2150,
79
+ adcBitDepth: 12, baseIso: 40, maximumIso: 6400, maximumAnalogGain: 64, readoutSeconds: 1 / 30,
80
+ fixedPattern: { prnu: .008, dsnu: .3, hotPixelFraction: 2e-4, hotPixelMultiplier: 50, rowElectrons: .2, columnElectrons: .2 },
81
+ goodFor: ["everyday", "travel", "family"], description: "A large phone sensor: tiny pixels with a small full well, so less dynamic range and more noise per pixel; simulated as a single frame (no multi-frame stacking).",
82
+ fixedLenses: "phone", provenance: { ...sourced("S9", "pixels", "pixelPitchMicrometers", "sensorSize", "architecture", "adcBitDepth"), fullWellElectrons: "design (S10 scaled by pixel area)",
83
+ readNoise: "high gain published S10, low gain design", highGainFullWellElectrons: "derived", readoutSeconds: "derived S9 (1 / full-resolution frame rate)",
84
+ fixedPattern: "PRNU, row and column published S10; the rest design", baseIso: "derived" } }
85
+ ] satisfies SensorProfile[]).map((p) => Object.freeze(p)))
86
+ ```