@photivra/engine 1.5.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +414 -6
- package/CITATION.cff +1 -1
- package/NODE_SUPPORT.md +64 -12
- package/README.md +301 -182
- package/SECURITY.md +37 -28
- package/THIRD_PARTY.md +148 -4
- package/dist/camera/equivalent-focal-length.d.ts +13 -14
- package/dist/camera/equivalent-focal-length.js +15 -13
- package/dist/camera/equivalent-focal-length.js.map +1 -1
- package/dist/camera/field-of-view.d.ts +30 -19
- package/dist/camera/field-of-view.js +67 -18
- package/dist/camera/field-of-view.js.map +1 -1
- package/dist/camera/lens-projection.d.ts +170 -0
- package/dist/camera/lens-projection.js +444 -0
- package/dist/camera/lens-projection.js.map +1 -0
- package/dist/camera/projected-object-size.d.ts +10 -11
- package/dist/camera/projected-object-size.js +12 -10
- package/dist/camera/projected-object-size.js.map +1 -1
- package/dist/capture/capture-export-metadata.d.ts +9 -10
- package/dist/capture/capture-export-metadata.js +8 -5
- package/dist/capture/capture-export-metadata.js.map +1 -1
- package/dist/capture/dng-import.d.ts +91 -0
- package/dist/capture/dng-import.js +343 -0
- package/dist/capture/dng-import.js.map +1 -0
- package/dist/capture/environment-raw-producer.d.ts +37 -17
- package/dist/capture/environment-raw-producer.js +43 -19
- package/dist/capture/environment-raw-producer.js.map +1 -1
- package/dist/capture/export-jpeg.d.ts +35 -4
- package/dist/capture/export-jpeg.js +133 -47
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/export-tiff.d.ts +48 -24
- package/dist/capture/export-tiff.js +91 -31
- package/dist/capture/export-tiff.js.map +1 -1
- package/dist/capture/focus-control.d.ts +42 -65
- package/dist/capture/focus-control.js +56 -75
- package/dist/capture/focus-control.js.map +1 -1
- package/dist/capture/host-yield-cadence.d.ts +25 -0
- package/dist/capture/host-yield-cadence.js +25 -0
- package/dist/capture/host-yield-cadence.js.map +1 -0
- package/dist/capture/linear-encoding.d.ts +14 -12
- package/dist/capture/linear-encoding.js +14 -13
- package/dist/capture/linear-encoding.js.map +1 -1
- package/dist/capture/lossless-jpeg.d.ts +27 -0
- package/dist/capture/lossless-jpeg.js +237 -0
- package/dist/capture/lossless-jpeg.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +227 -0
- package/dist/capture/native-environment-lean.js +974 -0
- package/dist/capture/native-environment-lean.js.map +1 -0
- package/dist/capture/native-environment-optical-stage.d.ts +97 -0
- package/dist/capture/native-environment-optical-stage.js +340 -0
- package/dist/capture/native-environment-optical-stage.js.map +1 -0
- package/dist/capture/native-environment-raw.d.ts +140 -6
- package/dist/capture/native-environment-raw.js +435 -34
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/capture/native-raw-development.d.ts +116 -9
- package/dist/capture/native-raw-development.js +221 -62
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/capture/native-raw-dng.d.ts +60 -0
- package/dist/capture/native-raw-dng.js +167 -0
- package/dist/capture/native-raw-dng.js.map +1 -0
- package/dist/capture/native-raw.d.ts +65 -12
- package/dist/capture/native-raw.js +81 -19
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/packed-sensor-raw.d.ts +160 -0
- package/dist/capture/packed-sensor-raw.js +221 -0
- package/dist/capture/packed-sensor-raw.js.map +1 -0
- package/dist/capture/photographic-export.d.ts +58 -35
- package/dist/capture/photographic-export.js +177 -70
- package/dist/capture/photographic-export.js.map +1 -1
- package/dist/capture/raw-development-kernel.d.ts +52 -0
- package/dist/capture/raw-development-kernel.js +88 -0
- package/dist/capture/raw-development-kernel.js.map +1 -0
- package/dist/capture/raw-frame-limits.d.ts +7 -7
- package/dist/capture/raw-frame-limits.js +7 -6
- package/dist/capture/raw-frame-limits.js.map +1 -1
- package/dist/capture/raw-frame-reconstruction.d.ts +8 -9
- package/dist/capture/raw-frame-reconstruction.js +9 -8
- package/dist/capture/raw-frame-reconstruction.js.map +1 -1
- package/dist/capture/raw-white-level.d.ts +55 -0
- package/dist/capture/raw-white-level.js +165 -0
- package/dist/capture/raw-white-level.js.map +1 -0
- package/dist/capture/release-sequence.d.ts +5 -7
- package/dist/capture/release-sequence.js +15 -13
- package/dist/capture/release-sequence.js.map +1 -1
- package/dist/capture/sensor-raw-frame.d.ts +5 -6
- package/dist/capture/sensor-raw-frame.js +26 -9
- package/dist/capture/sensor-raw-frame.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +21 -13
- package/dist/capture/sensor-raw-producer.js +86 -25
- package/dist/capture/sensor-raw-producer.js.map +1 -1
- package/dist/capture/simulated-capture.d.ts +15 -16
- package/dist/capture/simulated-capture.js +22 -12
- package/dist/capture/simulated-capture.js.map +1 -1
- package/dist/color/camera-color-matrix.d.ts +69 -0
- package/dist/color/camera-color-matrix.js +132 -0
- package/dist/color/camera-color-matrix.js.map +1 -0
- package/dist/color/capture-color.d.ts +17 -10
- package/dist/color/capture-color.js +50 -17
- package/dist/color/capture-color.js.map +1 -1
- package/dist/color/chromatic-adaptation.d.ts +63 -0
- package/dist/color/chromatic-adaptation.js +91 -0
- package/dist/color/chromatic-adaptation.js.map +1 -0
- package/dist/color/color-difference.d.ts +104 -0
- package/dist/color/color-difference.js +205 -0
- package/dist/color/color-difference.js.map +1 -0
- package/dist/color/color-matching-functions.d.ts +168 -0
- package/dist/color/color-matching-functions.js +309 -0
- package/dist/color/color-matching-functions.js.map +1 -0
- package/dist/color/colorimetry-math.d.ts +52 -0
- package/dist/color/colorimetry-math.js +144 -0
- package/dist/color/colorimetry-math.js.map +1 -0
- package/dist/color/gamut-mapping.d.ts +54 -0
- package/dist/color/gamut-mapping.js +107 -0
- package/dist/color/gamut-mapping.js.map +1 -0
- package/dist/color/illuminants.d.ts +182 -0
- package/dist/color/illuminants.js +434 -0
- package/dist/color/illuminants.js.map +1 -0
- package/dist/color/rgb-color-spaces.d.ts +79 -0
- package/dist/color/rgb-color-spaces.js +115 -0
- package/dist/color/rgb-color-spaces.js.map +1 -0
- package/dist/color/sensor-color-development.d.ts +33 -11
- package/dist/color/sensor-color-development.js +104 -30
- package/dist/color/sensor-color-development.js.map +1 -1
- package/dist/color/srgb-icc.d.ts +9 -2
- package/dist/color/srgb-icc.js +16 -5
- package/dist/color/srgb-icc.js.map +1 -1
- package/dist/color/srgb-transfer.d.ts +1 -2
- package/dist/color/srgb-transfer.js +1 -1
- package/dist/color/srgb-transfer.js.map +1 -1
- package/dist/color/white-balance.d.ts +68 -19
- package/dist/color/white-balance.js +168 -35
- package/dist/color/white-balance.js.map +1 -1
- package/dist/composition/image-formation-plan.d.ts +152 -128
- package/dist/composition/image-formation-plan.js +292 -138
- package/dist/composition/image-formation-plan.js.map +1 -1
- package/dist/composition/plan-consumer.d.ts +6 -7
- package/dist/composition/plan-consumer.js +14 -11
- package/dist/composition/plan-consumer.js.map +1 -1
- package/dist/core/calculation-result.d.ts +21 -14
- package/dist/core/calculation-result.js +209 -11
- package/dist/core/calculation-result.js.map +1 -1
- package/dist/core/canonical-json.d.ts +13 -8
- package/dist/core/canonical-json.js +34 -15
- package/dist/core/canonical-json.js.map +1 -1
- package/dist/core/configuration-error.d.ts +5 -4
- package/dist/core/configuration-error.js +5 -3
- package/dist/core/configuration-error.js.map +1 -1
- package/dist/core/evidence-provenance.d.ts +12 -12
- package/dist/core/evidence-provenance.js +10 -9
- package/dist/core/evidence-provenance.js.map +1 -1
- package/dist/core/image-formation.d.ts +11 -10
- package/dist/core/image-formation.js +11 -9
- package/dist/core/image-formation.js.map +1 -1
- package/dist/core/owned-data.d.ts +9 -17
- package/dist/core/owned-data.js +9 -16
- package/dist/core/owned-data.js.map +1 -1
- package/dist/core/portable-math.d.ts +15 -0
- package/dist/core/portable-math.js +56 -0
- package/dist/core/portable-math.js.map +1 -0
- package/dist/core/record-validation.d.ts +48 -9
- package/dist/core/record-validation.js +93 -19
- package/dist/core/record-validation.js.map +1 -1
- package/dist/core/scientific-assurance.d.ts +16 -14
- package/dist/core/scientific-assurance.js +22 -18
- package/dist/core/scientific-assurance.js.map +1 -1
- package/dist/core/spectral-composition.d.ts +13 -14
- package/dist/core/spectral-composition.js +30 -27
- package/dist/core/spectral-composition.js.map +1 -1
- package/dist/core/spectral.d.ts +5 -6
- package/dist/core/spectral.js +9 -7
- package/dist/core/spectral.js.map +1 -1
- package/dist/core/validation.d.ts +3 -6
- package/dist/core/validation.js +3 -5
- package/dist/core/validation.js.map +1 -1
- package/dist/core/version.d.ts +6 -6
- package/dist/core/version.js +6 -5
- package/dist/core/version.js.map +1 -1
- package/dist/equipment/exposure-capabilities.d.ts +10 -10
- package/dist/equipment/exposure-capabilities.js +40 -30
- package/dist/equipment/exposure-capabilities.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +13 -6
- package/dist/equipment/generic-tier-assets.js +34 -15
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/generic-tier-presets.d.ts +5 -6
- package/dist/equipment/generic-tier-presets.js +15 -13
- package/dist/equipment/generic-tier-presets.js.map +1 -1
- package/dist/equipment/iso-capabilities.d.ts +21 -22
- package/dist/equipment/iso-capabilities.js +49 -44
- package/dist/equipment/iso-capabilities.js.map +1 -1
- package/dist/equipment/metering-capabilities.d.ts +5 -6
- package/dist/equipment/metering-capabilities.js +22 -19
- package/dist/equipment/metering-capabilities.js.map +1 -1
- package/dist/equipment/release-capabilities.d.ts +3 -4
- package/dist/equipment/release-capabilities.js +18 -14
- package/dist/equipment/release-capabilities.js.map +1 -1
- package/dist/exposure/exposure-duration-control.d.ts +19 -17
- package/dist/exposure/exposure-duration-control.js +27 -23
- package/dist/exposure/exposure-duration-control.js.map +1 -1
- package/dist/exposure/exposure-mode-resolver.d.ts +42 -39
- package/dist/exposure/exposure-mode-resolver.js +127 -82
- package/dist/exposure/exposure-mode-resolver.js.map +1 -1
- package/dist/exposure/exposure.d.ts +17 -16
- package/dist/exposure/exposure.js +22 -15
- package/dist/exposure/exposure.js.map +1 -1
- package/dist/exposure/flash-ttl.d.ts +135 -0
- package/dist/exposure/flash-ttl.js +193 -0
- package/dist/exposure/flash-ttl.js.map +1 -0
- package/dist/exposure/flash.d.ts +144 -36
- package/dist/exposure/flash.js +259 -59
- package/dist/exposure/flash.js.map +1 -1
- package/dist/exposure/metering-scene-radiance.d.ts +10 -10
- package/dist/exposure/metering-scene-radiance.js +42 -23
- package/dist/exposure/metering-scene-radiance.js.map +1 -1
- package/dist/exposure/metering-target.d.ts +8 -8
- package/dist/exposure/metering-target.js +30 -20
- package/dist/exposure/metering-target.js.map +1 -1
- package/dist/exposure/metering-temporal.d.ts +16 -15
- package/dist/exposure/metering-temporal.js +41 -19
- package/dist/exposure/metering-temporal.js.map +1 -1
- package/dist/exposure/metering.d.ts +64 -17
- package/dist/exposure/metering.js +159 -30
- package/dist/exposure/metering.js.map +1 -1
- package/dist/exposure/program-line.d.ts +5 -5
- package/dist/exposure/program-line.js +20 -13
- package/dist/exposure/program-line.js.map +1 -1
- package/dist/facade/camera-preset.d.ts +8 -0
- package/dist/facade/camera-preset.js +102 -0
- package/dist/facade/camera-preset.js.map +1 -0
- package/dist/facade/chart-radiance-batch.d.ts +57 -0
- package/dist/facade/chart-radiance-batch.js +121 -0
- package/dist/facade/chart-radiance-batch.js.map +1 -0
- package/dist/facade/chart-scenes.d.ts +177 -0
- package/dist/facade/chart-scenes.js +232 -0
- package/dist/facade/chart-scenes.js.map +1 -0
- package/dist/facade/diffraction-provider.d.ts +30 -0
- package/dist/facade/diffraction-provider.js +127 -0
- package/dist/facade/diffraction-provider.js.map +1 -0
- package/dist/facade/host-yield.d.ts +7 -0
- package/dist/facade/host-yield.js +19 -0
- package/dist/facade/host-yield.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +117 -0
- package/dist/facade/scene-radiance.js +205 -0
- package/dist/facade/scene-radiance.js.map +1 -0
- package/dist/facade/simulate-capture.d.ts +363 -0
- package/dist/facade/simulate-capture.js +1281 -0
- package/dist/facade/simulate-capture.js.map +1 -0
- package/dist/gpu/chart-radiance-gpu.d.ts +17 -0
- package/dist/gpu/chart-radiance-gpu.js +176 -0
- package/dist/gpu/chart-radiance-gpu.js.map +1 -0
- package/dist/gpu/image-kernel-accelerator.d.ts +132 -0
- package/dist/gpu/image-kernel-accelerator.js +720 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -0
- package/dist/gpu/image-kernel-execution.d.ts +92 -0
- package/dist/gpu/image-kernel-execution.js +84 -0
- package/dist/gpu/image-kernel-execution.js.map +1 -0
- package/dist/gpu/sensor-stage-gpu.d.ts +44 -0
- package/dist/gpu/sensor-stage-gpu.js +317 -0
- package/dist/gpu/sensor-stage-gpu.js.map +1 -0
- package/dist/gpu/temporal-sum-gpu.d.ts +23 -0
- package/dist/gpu/temporal-sum-gpu.js +150 -0
- package/dist/gpu/temporal-sum-gpu.js.map +1 -0
- package/dist/gpu/webgpu-runtime.d.ts +63 -0
- package/dist/gpu/webgpu-runtime.js +215 -0
- package/dist/gpu/webgpu-runtime.js.map +1 -0
- package/dist/gpu/wgsl-chart-kernel.d.ts +30 -0
- package/dist/gpu/wgsl-chart-kernel.js +165 -0
- package/dist/gpu/wgsl-chart-kernel.js.map +1 -0
- package/dist/gpu/wgsl-kernels.d.ts +53 -0
- package/dist/gpu/wgsl-kernels.js +365 -0
- package/dist/gpu/wgsl-kernels.js.map +1 -0
- package/dist/gpu/wgsl-sensor-kernel.d.ts +64 -0
- package/dist/gpu/wgsl-sensor-kernel.js +409 -0
- package/dist/gpu/wgsl-sensor-kernel.js.map +1 -0
- package/dist/index.d.ts +91 -25
- package/dist/index.js +60 -15
- package/dist/index.js.map +1 -1
- package/dist/motion/camera-rotation.d.ts +92 -40
- package/dist/motion/camera-rotation.js +84 -47
- package/dist/motion/camera-rotation.js.map +1 -1
- package/dist/motion/capture-rotation-inverse-mapping.d.ts +37 -34
- package/dist/motion/capture-rotation-inverse-mapping.js +32 -24
- package/dist/motion/capture-rotation-inverse-mapping.js.map +1 -1
- package/dist/motion/capture-rotation-temporal-quadrature.d.ts +39 -35
- package/dist/motion/capture-rotation-temporal-quadrature.js +37 -28
- package/dist/motion/capture-rotation-temporal-quadrature.js.map +1 -1
- package/dist/motion/capture-rotation-trajectory.d.ts +32 -28
- package/dist/motion/capture-rotation-trajectory.js +36 -26
- package/dist/motion/capture-rotation-trajectory.js.map +1 -1
- package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts +23 -21
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js +33 -14
- package/dist/motion/capture-translation-parallax-temporal-quadrature.js.map +1 -1
- package/dist/motion/extended-object-projection.d.ts +19 -16
- package/dist/motion/extended-object-projection.js +38 -18
- package/dist/motion/extended-object-projection.js.map +1 -1
- package/dist/motion/projected-motion.d.ts +18 -17
- package/dist/motion/projected-motion.js +25 -17
- package/dist/motion/projected-motion.js.map +1 -1
- package/dist/motion/temporal-exposure-sum.d.ts +55 -0
- package/dist/motion/temporal-exposure-sum.js +141 -0
- package/dist/motion/temporal-exposure-sum.js.map +1 -0
- package/dist/motion/video-sequence.d.ts +111 -0
- package/dist/motion/video-sequence.js +179 -0
- package/dist/motion/video-sequence.js.map +1 -0
- package/dist/optics/aperture.d.ts +14 -15
- package/dist/optics/aperture.js +13 -11
- package/dist/optics/aperture.js.map +1 -1
- package/dist/optics/atmospheric-haze.d.ts +83 -0
- package/dist/optics/atmospheric-haze.js +113 -0
- package/dist/optics/atmospheric-haze.js.map +1 -0
- package/dist/optics/brown-conrady-distortion.d.ts +116 -0
- package/dist/optics/brown-conrady-distortion.js +291 -0
- package/dist/optics/brown-conrady-distortion.js.map +1 -0
- package/dist/optics/circle-of-confusion.d.ts +10 -10
- package/dist/optics/circle-of-confusion.js +15 -11
- package/dist/optics/circle-of-confusion.js.map +1 -1
- package/dist/optics/complex-pupil-psf.d.ts +64 -33
- package/dist/optics/complex-pupil-psf.js +190 -98
- package/dist/optics/complex-pupil-psf.js.map +1 -1
- package/dist/optics/depth-of-field.d.ts +25 -15
- package/dist/optics/depth-of-field.js +67 -27
- package/dist/optics/depth-of-field.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +118 -0
- package/dist/optics/diffraction-psf-profile.js +252 -0
- package/dist/optics/diffraction-psf-profile.js.map +1 -0
- package/dist/optics/diffraction.d.ts +14 -13
- package/dist/optics/diffraction.js +14 -10
- package/dist/optics/diffraction.js.map +1 -1
- package/dist/optics/focus-breathing.d.ts +88 -27
- package/dist/optics/focus-breathing.js +114 -23
- package/dist/optics/focus-breathing.js.map +1 -1
- package/dist/optics/focus-state.d.ts +18 -18
- package/dist/optics/focus-state.js +21 -15
- package/dist/optics/focus-state.js.map +1 -1
- package/dist/optics/front-of-lens-filter.d.ts +45 -14
- package/dist/optics/front-of-lens-filter.js +259 -88
- package/dist/optics/front-of-lens-filter.js.map +1 -1
- package/dist/optics/illumination-vignetting.d.ts +16 -17
- package/dist/optics/illumination-vignetting.js +40 -17
- package/dist/optics/illumination-vignetting.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +59 -0
- package/dist/optics/irradiance-convolution.js +138 -0
- package/dist/optics/irradiance-convolution.js.map +1 -0
- package/dist/optics/lateral-chromatic-aberration.d.ts +33 -30
- package/dist/optics/lateral-chromatic-aberration.js +33 -23
- package/dist/optics/lateral-chromatic-aberration.js.map +1 -1
- package/dist/optics/lens-psf-kernels.d.ts +89 -0
- package/dist/optics/lens-psf-kernels.js +175 -0
- package/dist/optics/lens-psf-kernels.js.map +1 -0
- package/dist/optics/lens-psf-profile.d.ts +78 -46
- package/dist/optics/lens-psf-profile.js +335 -124
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/optics/longitudinal-chromatic-aberration.d.ts +102 -0
- package/dist/optics/longitudinal-chromatic-aberration.js +210 -0
- package/dist/optics/longitudinal-chromatic-aberration.js.map +1 -0
- package/dist/optics/polygon-diffraction.d.ts +80 -23
- package/dist/optics/polygon-diffraction.js +130 -25
- package/dist/optics/polygon-diffraction.js.map +1 -1
- package/dist/optics/profile-contract.d.ts +5 -6
- package/dist/optics/profile-contract.js +14 -11
- package/dist/optics/profile-contract.js.map +1 -1
- package/dist/optics/psf-foundation.d.ts +23 -23
- package/dist/optics/psf-foundation.js +24 -21
- package/dist/optics/psf-foundation.js.map +1 -1
- package/dist/optics/pupil-vignetting.d.ts +118 -0
- package/dist/optics/pupil-vignetting.js +264 -0
- package/dist/optics/pupil-vignetting.js.map +1 -0
- package/dist/optics/radial-distortion.d.ts +34 -33
- package/dist/optics/radial-distortion.js +37 -30
- package/dist/optics/radial-distortion.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +47 -33
- package/dist/optics/scene-to-sensor-irradiance.js +73 -45
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/scene-to-sensor-quadrature.d.ts +23 -18
- package/dist/optics/scene-to-sensor-quadrature.js +16 -11
- package/dist/optics/scene-to-sensor-quadrature.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.d.ts +20 -4
- package/dist/optics/sensor-aperture-rays.js +32 -14
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.d.ts +81 -16
- package/dist/optics/sensor-environment-query.js +145 -12
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/sensor-psf-quadrature.d.ts +12 -10
- package/dist/optics/sensor-psf-quadrature.js +18 -11
- package/dist/optics/sensor-psf-quadrature.js.map +1 -1
- package/dist/optics/sensor-psf-support.d.ts +8 -6
- package/dist/optics/sensor-psf-support.js +8 -5
- package/dist/optics/sensor-psf-support.js.map +1 -1
- package/dist/optics/stray-light.d.ts +22 -19
- package/dist/optics/stray-light.js +18 -12
- package/dist/optics/stray-light.js.map +1 -1
- package/dist/optics/thin-lens.d.ts +10 -9
- package/dist/optics/thin-lens.js +12 -8
- package/dist/optics/thin-lens.js.map +1 -1
- package/dist/optics/zernike-wavefront.d.ts +154 -0
- package/dist/optics/zernike-wavefront.js +352 -0
- package/dist/optics/zernike-wavefront.js.map +1 -0
- package/dist/output/capture-corrected-sdr.d.ts +16 -8
- package/dist/output/capture-corrected-sdr.js +16 -8
- package/dist/output/capture-corrected-sdr.js.map +1 -1
- package/dist/output/capture-geometry.d.ts +34 -36
- package/dist/output/capture-geometry.js +52 -32
- package/dist/output/capture-geometry.js.map +1 -1
- package/dist/output/capture-sdr.d.ts +7 -6
- package/dist/output/capture-sdr.js +11 -8
- package/dist/output/capture-sdr.js.map +1 -1
- package/dist/output/crop.d.ts +22 -6
- package/dist/output/crop.js +40 -6
- package/dist/output/crop.js.map +1 -1
- package/dist/output/geometric-transforms.d.ts +60 -23
- package/dist/output/geometric-transforms.js +111 -24
- package/dist/output/geometric-transforms.js.map +1 -1
- package/dist/output/lens-corrections.d.ts +27 -19
- package/dist/output/lens-corrections.js +29 -13
- package/dist/output/lens-corrections.js.map +1 -1
- package/dist/output/native-capture-sdr.d.ts +71 -16
- package/dist/output/native-capture-sdr.js +190 -33
- package/dist/output/native-capture-sdr.js.map +1 -1
- package/dist/output/png-encoder.d.ts +65 -0
- package/dist/output/png-encoder.js +461 -0
- package/dist/output/png-encoder.js.map +1 -0
- package/dist/output/print-contrast-reference.d.ts +10 -9
- package/dist/output/print-contrast-reference.js +10 -8
- package/dist/output/print-contrast-reference.js.map +1 -1
- package/dist/output/print-detail.d.ts +18 -17
- package/dist/output/print-detail.js +12 -10
- package/dist/output/print-detail.js.map +1 -1
- package/dist/output/print-jpeg.d.ts +26 -5
- package/dist/output/print-jpeg.js +117 -50
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/output/print-plan.d.ts +19 -19
- package/dist/output/print-plan.js +11 -10
- package/dist/output/print-plan.js.map +1 -1
- package/dist/output/print-region-statistics.d.ts +14 -15
- package/dist/output/print-region-statistics.js +11 -11
- package/dist/output/print-region-statistics.js.map +1 -1
- package/dist/output/sdr-rendering.d.ts +48 -10
- package/dist/output/sdr-rendering.js +114 -25
- package/dist/output/sdr-rendering.js.map +1 -1
- package/dist/output/subject-framing-crop.d.ts +6 -10
- package/dist/output/subject-framing-crop.js +23 -13
- package/dist/output/subject-framing-crop.js.map +1 -1
- package/dist/processing/demosaic.d.ts +90 -0
- package/dist/processing/demosaic.js +275 -0
- package/dist/processing/demosaic.js.map +1 -0
- package/dist/processing/denoise.d.ts +68 -0
- package/dist/processing/denoise.js +214 -0
- package/dist/processing/denoise.js.map +1 -0
- package/dist/processing/focus-stacking.d.ts +17 -0
- package/dist/processing/focus-stacking.js +80 -0
- package/dist/processing/focus-stacking.js.map +1 -0
- package/dist/processing/hdr.d.ts +54 -0
- package/dist/processing/hdr.js +172 -0
- package/dist/processing/hdr.js.map +1 -0
- package/dist/processing/image-ops.d.ts +66 -0
- package/dist/processing/image-ops.js +192 -0
- package/dist/processing/image-ops.js.map +1 -0
- package/dist/processing/panorama.d.ts +51 -0
- package/dist/processing/panorama.js +305 -0
- package/dist/processing/panorama.js.map +1 -0
- package/dist/processing/processing-task.d.ts +85 -0
- package/dist/processing/processing-task.js +164 -0
- package/dist/processing/processing-task.js.map +1 -0
- package/dist/processing/tone-mapping.d.ts +69 -0
- package/dist/processing/tone-mapping.js +310 -0
- package/dist/processing/tone-mapping.js.map +1 -0
- package/dist/schema/camera.d.ts +5 -4
- package/dist/schema/illumination-spectral.d.ts +13 -19
- package/dist/schema/illumination-spectral.js +23 -19
- package/dist/schema/illumination-spectral.js.map +1 -1
- package/dist/schema/illumination-temporal.d.ts +12 -15
- package/dist/schema/illumination-temporal.js +43 -28
- package/dist/schema/illumination-temporal.js.map +1 -1
- package/dist/schema/illumination.d.ts +5 -4
- package/dist/schema/illumination.js +55 -20
- package/dist/schema/illumination.js.map +1 -1
- package/dist/schema/scene-radiance-temporal.d.ts +9 -11
- package/dist/schema/scene-radiance-temporal.js +26 -23
- package/dist/schema/scene-radiance-temporal.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +17 -17
- package/dist/schema/scene-radiance.js +56 -34
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/schema/scene.d.ts +14 -13
- package/dist/schema/validation.d.ts +0 -1
- package/dist/schema/validation.js +17 -15
- package/dist/schema/validation.js.map +1 -1
- package/dist/sensor/accumulated-charge.d.ts +45 -64
- package/dist/sensor/accumulated-charge.js +41 -47
- package/dist/sensor/accumulated-charge.js.map +1 -1
- package/dist/sensor/architecture.d.ts +22 -25
- package/dist/sensor/architecture.js +17 -16
- package/dist/sensor/architecture.js.map +1 -1
- package/dist/sensor/camera-saturation-capacity.d.ts +21 -28
- package/dist/sensor/camera-saturation-capacity.js +36 -34
- package/dist/sensor/camera-saturation-capacity.js.map +1 -1
- package/dist/sensor/capture-color-sampling-binding.d.ts +46 -51
- package/dist/sensor/capture-color-sampling-binding.js +39 -41
- package/dist/sensor/capture-color-sampling-binding.js.map +1 -1
- package/dist/sensor/capture-mode-timing.d.ts +8 -9
- package/dist/sensor/capture-mode-timing.js +24 -21
- package/dist/sensor/capture-mode-timing.js.map +1 -1
- package/dist/sensor/capture-mode.d.ts +28 -30
- package/dist/sensor/capture-mode.js +33 -31
- package/dist/sensor/capture-mode.js.map +1 -1
- package/dist/sensor/color-sampling.d.ts +30 -33
- package/dist/sensor/color-sampling.js +32 -37
- package/dist/sensor/color-sampling.js.map +1 -1
- package/dist/sensor/constant-rate-temporal-integration.d.ts +19 -23
- package/dist/sensor/constant-rate-temporal-integration.js +28 -26
- package/dist/sensor/constant-rate-temporal-integration.js.map +1 -1
- package/dist/sensor/dark-current.d.ts +53 -49
- package/dist/sensor/dark-current.js +87 -46
- package/dist/sensor/dark-current.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +71 -30
- package/dist/sensor/environment-photo-signal.js +127 -46
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/dist/sensor/eqe-electron-rate.d.ts +14 -13
- package/dist/sensor/eqe-electron-rate.js +34 -14
- package/dist/sensor/eqe-electron-rate.js.map +1 -1
- package/dist/sensor/eqe-local-exposure.d.ts +24 -24
- package/dist/sensor/eqe-local-exposure.js +15 -11
- package/dist/sensor/eqe-local-exposure.js.map +1 -1
- package/dist/sensor/eqe-temporal-exposure.d.ts +11 -11
- package/dist/sensor/eqe-temporal-exposure.js +15 -10
- package/dist/sensor/eqe-temporal-exposure.js.map +1 -1
- package/dist/sensor/exposure-window.d.ts +48 -34
- package/dist/sensor/exposure-window.js +77 -36
- package/dist/sensor/exposure-window.js.map +1 -1
- package/dist/sensor/fixed-pattern-noise.d.ts +108 -0
- package/dist/sensor/fixed-pattern-noise.js +166 -0
- package/dist/sensor/fixed-pattern-noise.js.map +1 -0
- package/dist/sensor/iso-gain.d.ts +44 -0
- package/dist/sensor/iso-gain.js +119 -0
- package/dist/sensor/iso-gain.js.map +1 -0
- package/dist/sensor/iso-signal-chain.d.ts +25 -48
- package/dist/sensor/iso-signal-chain.js +52 -66
- package/dist/sensor/iso-signal-chain.js.map +1 -1
- package/dist/sensor/local-exposure-binding.d.ts +21 -21
- package/dist/sensor/local-exposure-binding.js +22 -18
- package/dist/sensor/local-exposure-binding.js.map +1 -1
- package/dist/sensor/optical-stack.d.ts +36 -40
- package/dist/sensor/optical-stack.js +34 -37
- package/dist/sensor/optical-stack.js.map +1 -1
- package/dist/sensor/photon-energy.d.ts +22 -28
- package/dist/sensor/photon-energy.js +42 -34
- package/dist/sensor/photon-energy.js.map +1 -1
- package/dist/sensor/physical-charge-capacity.d.ts +20 -30
- package/dist/sensor/physical-charge-capacity.js +35 -36
- package/dist/sensor/physical-charge-capacity.js.map +1 -1
- package/dist/sensor/pixel-pitch.d.ts +2 -3
- package/dist/sensor/pixel-pitch.js +4 -2
- package/dist/sensor/pixel-pitch.js.map +1 -1
- package/dist/sensor/radiometry-readiness.d.ts +13 -15
- package/dist/sensor/radiometry-readiness.js +20 -17
- package/dist/sensor/radiometry-readiness.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +173 -83
- package/dist/sensor/raw-readout.js +425 -160
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/raw-reconstruction.d.ts +112 -44
- package/dist/sensor/raw-reconstruction.js +200 -47
- package/dist/sensor/raw-reconstruction.js.map +1 -1
- package/dist/sensor/readout-exposure-linkage.d.ts +103 -34
- package/dist/sensor/readout-exposure-linkage.js +125 -27
- package/dist/sensor/readout-exposure-linkage.js.map +1 -1
- package/dist/sensor/readout-timing.d.ts +43 -44
- package/dist/sensor/readout-timing.js +38 -30
- package/dist/sensor/readout-timing.js.map +1 -1
- package/dist/sensor/response-application-compatibility.d.ts +21 -28
- package/dist/sensor/response-application-compatibility.js +35 -34
- package/dist/sensor/response-application-compatibility.js.map +1 -1
- package/dist/sensor/response-operating-range.d.ts +26 -35
- package/dist/sensor/response-operating-range.js +40 -39
- package/dist/sensor/response-operating-range.js.map +1 -1
- package/dist/sensor/responsivity-photocurrent.d.ts +21 -29
- package/dist/sensor/responsivity-photocurrent.js +57 -40
- package/dist/sensor/responsivity-photocurrent.js.map +1 -1
- package/dist/sensor/sampling-aperture.d.ts +21 -25
- package/dist/sensor/sampling-aperture.js +29 -28
- package/dist/sensor/sampling-aperture.js.map +1 -1
- package/dist/sensor/scene-eqe-temporal-exposure.d.ts +14 -12
- package/dist/sensor/scene-eqe-temporal-exposure.js +13 -8
- package/dist/sensor/scene-eqe-temporal-exposure.js.map +1 -1
- package/dist/sensor/sensor-geometry.d.ts +28 -32
- package/dist/sensor/sensor-geometry.js +35 -20
- package/dist/sensor/sensor-geometry.js.map +1 -1
- package/dist/sensor/signal-noise.d.ts +14 -16
- package/dist/sensor/signal-noise.js +15 -13
- package/dist/sensor/signal-noise.js.map +1 -1
- package/dist/sensor/spatial-sample-reduction.d.ts +45 -60
- package/dist/sensor/spatial-sample-reduction.js +39 -48
- package/dist/sensor/spatial-sample-reduction.js.map +1 -1
- package/dist/sensor/spatial-sampling-quadrature.d.ts +46 -54
- package/dist/sensor/spatial-sampling-quadrature.js +31 -36
- package/dist/sensor/spatial-sampling-quadrature.js.map +1 -1
- package/dist/sensor/spatio-spectral-reduction.d.ts +32 -42
- package/dist/sensor/spatio-spectral-reduction.js +24 -29
- package/dist/sensor/spatio-spectral-reduction.js.map +1 -1
- package/dist/sensor/spectral-coverage.d.ts +6 -9
- package/dist/sensor/spectral-coverage.js +9 -6
- package/dist/sensor/spectral-coverage.js.map +1 -1
- package/dist/sensor/spectral-quadrature.d.ts +46 -60
- package/dist/sensor/spectral-quadrature.js +38 -48
- package/dist/sensor/spectral-quadrature.js.map +1 -1
- package/dist/sensor/spectral-response.d.ts +53 -41
- package/dist/sensor/spectral-response.js +80 -47
- package/dist/sensor/spectral-response.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +50 -0
- package/dist/sensor/stochastic-seed.js +123 -0
- package/dist/sensor/stochastic-seed.js.map +1 -0
- package/dist/sensor/temporal-eqe-sum.d.ts +2 -4
- package/dist/sensor/temporal-eqe-sum.js +5 -6
- package/dist/sensor/temporal-eqe-sum.js.map +1 -1
- package/dist/sensor/temporal-photo-signal.d.ts +5 -6
- package/dist/sensor/temporal-photo-signal.js +11 -10
- package/dist/sensor/temporal-photo-signal.js.map +1 -1
- package/dist/sensor/temporal-sample-binding.d.ts +1 -2
- package/dist/sensor/temporal-sample-binding.js +3 -3
- package/dist/sensor/temporal-sample-binding.js.map +1 -1
- package/dist/simulation/poc-simulation.d.ts +16 -16
- package/dist/simulation/poc-simulation.js +20 -15
- package/dist/simulation/poc-simulation.js.map +1 -1
- package/dist/stabilization/camera-shake.d.ts +42 -18
- package/dist/stabilization/camera-shake.js +43 -20
- package/dist/stabilization/camera-shake.js.map +1 -1
- package/dist/stabilization/system.d.ts +44 -41
- package/dist/stabilization/system.js +196 -108
- package/dist/stabilization/system.js.map +1 -1
- package/docs/AGENT_GUIDE.md +183 -0
- package/docs/API_REFERENCE.md +777 -2
- package/docs/QUICK_START.md +663 -0
- package/docs/USAGE.md +1719 -714
- package/docs/api/camera-equivalent-focal-length.md +6 -7
- package/docs/api/camera-field-of-view.md +16 -8
- package/docs/api/camera-lens-projection.md +246 -0
- package/docs/api/camera-projected-object-size.md +6 -5
- package/docs/api/capture-capture-export-metadata.md +10 -10
- package/docs/api/capture-dng-import.md +134 -0
- package/docs/api/capture-environment-raw-producer.md +17 -11
- package/docs/api/capture-export-jpeg.md +16 -0
- package/docs/api/capture-focus-control.md +25 -47
- package/docs/api/capture-linear-encoding.md +21 -12
- package/docs/api/capture-lossless-jpeg.md +49 -0
- package/docs/api/capture-native-environment-optical-stage.md +75 -0
- package/docs/api/capture-native-environment-raw.md +161 -4
- package/docs/api/capture-native-raw-development.md +69 -8
- package/docs/api/capture-native-raw-dng.md +84 -0
- package/docs/api/capture-native-raw.md +21 -10
- package/docs/api/capture-packed-sensor-raw.md +156 -0
- package/docs/api/capture-photographic-export.md +69 -31
- package/docs/api/capture-raw-frame-reconstruction.md +8 -8
- package/docs/api/capture-raw-white-level.md +37 -0
- package/docs/api/capture-release-sequence.md +6 -7
- package/docs/api/capture-sensor-raw-frame.md +6 -6
- package/docs/api/capture-sensor-raw-producer.md +17 -11
- package/docs/api/capture-simulated-capture.md +15 -15
- package/docs/api/color-camera-color-matrix.md +103 -0
- package/docs/api/color-capture-color.md +22 -11
- package/docs/api/color-chromatic-adaptation.md +82 -0
- package/docs/api/color-color-difference.md +134 -0
- package/docs/api/color-color-matching-functions.md +259 -0
- package/docs/api/color-colorimetry-math.md +54 -0
- package/docs/api/color-gamut-mapping.md +72 -0
- package/docs/api/color-illuminants.md +280 -0
- package/docs/api/color-rgb-color-spaces.md +151 -0
- package/docs/api/color-sensor-color-development.md +27 -0
- package/docs/api/color-srgb-icc.md +2 -4
- package/docs/api/color-white-balance.md +95 -19
- package/docs/api/composition-image-formation-plan.md +124 -95
- package/docs/api/composition-plan-consumer.md +7 -7
- package/docs/api/core-calculation-result.md +18 -14
- package/docs/api/core-configuration-error.md +1 -1
- package/docs/api/core-image-formation.md +7 -7
- package/docs/api/core-scientific-assurance.md +12 -10
- package/docs/api/core-spectral-composition.md +14 -14
- package/docs/api/core-spectral.md +6 -6
- package/docs/api/core-validation.md +3 -3
- package/docs/api/core-version.md +7 -6
- package/docs/api/equipment-exposure-capabilities.md +11 -10
- package/docs/api/equipment-generic-tier-assets.md +7 -2
- package/docs/api/equipment-generic-tier-presets.md +6 -6
- package/docs/api/equipment-iso-capabilities.md +18 -18
- package/docs/api/equipment-metering-capabilities.md +6 -6
- package/docs/api/equipment-release-capabilities.md +4 -4
- package/docs/api/exports.json +2253 -3
- package/docs/api/exposure-exposure-duration-control.md +9 -9
- package/docs/api/exposure-exposure-mode-resolver.md +35 -31
- package/docs/api/exposure-exposure.md +10 -10
- package/docs/api/exposure-flash-ttl.md +182 -0
- package/docs/api/exposure-flash.md +183 -28
- package/docs/api/exposure-metering-scene-radiance.md +11 -10
- package/docs/api/exposure-metering-target.md +9 -8
- package/docs/api/exposure-metering-temporal.md +8 -8
- package/docs/api/exposure-metering.md +69 -9
- package/docs/api/exposure-program-line.md +6 -5
- package/docs/api/facade-chart-radiance-batch.md +96 -0
- package/docs/api/facade-chart-scenes.md +240 -0
- package/docs/api/facade-scene-radiance.md +122 -0
- package/docs/api/facade-simulate-capture.md +397 -0
- package/docs/api/gpu-image-kernel-accelerator.md +157 -0
- package/docs/api/gpu-image-kernel-execution.md +152 -0
- package/docs/api/motion-camera-rotation.md +43 -26
- package/docs/api/motion-capture-rotation-inverse-mapping.md +24 -23
- package/docs/api/motion-capture-rotation-temporal-quadrature.md +22 -22
- package/docs/api/motion-capture-rotation-trajectory.md +15 -15
- package/docs/api/motion-capture-translation-parallax-temporal-quadrature.md +15 -14
- package/docs/api/motion-extended-object-projection.md +10 -8
- package/docs/api/motion-projected-motion.md +8 -8
- package/docs/api/motion-temporal-exposure-sum.md +98 -0
- package/docs/api/motion-video-sequence.md +159 -0
- package/docs/api/optics-aperture.md +8 -8
- package/docs/api/optics-atmospheric-haze.md +104 -0
- package/docs/api/optics-brown-conrady-distortion.md +123 -0
- package/docs/api/optics-circle-of-confusion.md +5 -4
- package/docs/api/optics-complex-pupil-psf.md +57 -16
- package/docs/api/optics-depth-of-field.md +12 -6
- package/docs/api/optics-diffraction-psf-profile.md +96 -0
- package/docs/api/optics-diffraction.md +8 -7
- package/docs/api/optics-focus-breathing.md +104 -16
- package/docs/api/optics-focus-state.md +9 -10
- package/docs/api/optics-front-of-lens-filter.md +72 -14
- package/docs/api/optics-illumination-vignetting.md +10 -10
- package/docs/api/optics-irradiance-convolution.md +95 -0
- package/docs/api/optics-lateral-chromatic-aberration.md +22 -21
- package/docs/api/optics-lens-psf-kernels.md +72 -0
- package/docs/api/optics-lens-psf-profile.md +48 -26
- package/docs/api/optics-longitudinal-chromatic-aberration.md +128 -0
- package/docs/api/optics-polygon-diffraction.md +78 -13
- package/docs/api/optics-profile-contract.md +4 -4
- package/docs/api/optics-psf-foundation.md +12 -12
- package/docs/api/optics-pupil-vignetting.md +132 -0
- package/docs/api/optics-radial-distortion.md +12 -12
- package/docs/api/optics-scene-to-sensor-irradiance.md +25 -14
- package/docs/api/optics-scene-to-sensor-quadrature.md +15 -12
- package/docs/api/optics-sensor-aperture-rays.md +3 -2
- package/docs/api/optics-sensor-environment-query.md +52 -9
- package/docs/api/optics-sensor-psf-quadrature.md +13 -10
- package/docs/api/optics-stray-light.md +12 -11
- package/docs/api/optics-thin-lens.md +4 -3
- package/docs/api/optics-zernike-wavefront.md +229 -0
- package/docs/api/output-capture-corrected-sdr.md +10 -7
- package/docs/api/output-capture-geometry.md +35 -36
- package/docs/api/output-capture-sdr.md +4 -4
- package/docs/api/output-crop.md +7 -4
- package/docs/api/output-geometric-transforms.md +65 -19
- package/docs/api/output-lens-corrections.md +17 -15
- package/docs/api/output-native-capture-sdr.md +26 -13
- package/docs/api/output-png-encoder.md +105 -0
- package/docs/api/output-print-contrast-reference.md +8 -8
- package/docs/api/output-print-detail.md +14 -14
- package/docs/api/output-print-jpeg.md +22 -5
- package/docs/api/output-print-plan.md +18 -18
- package/docs/api/output-print-region-statistics.md +14 -14
- package/docs/api/output-sdr-rendering.md +52 -10
- package/docs/api/output-subject-framing-crop.md +5 -6
- package/docs/api/processing-demosaic.md +120 -0
- package/docs/api/processing-denoise.md +148 -0
- package/docs/api/processing-focus-stacking.md +41 -0
- package/docs/api/processing-hdr.md +113 -0
- package/docs/api/processing-panorama.md +82 -0
- package/docs/api/processing-processing-task.md +107 -0
- package/docs/api/processing-tone-mapping.md +142 -0
- package/docs/api/schema-camera.md +6 -4
- package/docs/api/schema-illumination-spectral.md +10 -11
- package/docs/api/schema-illumination-temporal.md +13 -15
- package/docs/api/schema-illumination.md +4 -4
- package/docs/api/schema-scene-radiance-temporal.md +10 -11
- package/docs/api/schema-scene-radiance.md +18 -17
- package/docs/api/schema-scene.md +9 -9
- package/docs/api/schema-validation.md +1 -1
- package/docs/api/sensor-accumulated-charge.md +12 -20
- package/docs/api/sensor-architecture.md +15 -16
- package/docs/api/sensor-camera-saturation-capacity.md +9 -11
- package/docs/api/sensor-capture-color-sampling-binding.md +47 -51
- package/docs/api/sensor-capture-mode-timing.md +9 -9
- package/docs/api/sensor-capture-mode.md +29 -30
- package/docs/api/sensor-color-sampling.md +31 -33
- package/docs/api/sensor-constant-rate-temporal-integration.md +10 -11
- package/docs/api/sensor-dark-current.md +30 -14
- package/docs/api/sensor-environment-photo-signal.md +13 -6
- package/docs/api/sensor-eqe-electron-rate.md +9 -8
- package/docs/api/sensor-eqe-local-exposure.md +18 -17
- package/docs/api/sensor-eqe-temporal-exposure.md +12 -11
- package/docs/api/sensor-exposure-window.md +24 -24
- package/docs/api/sensor-fixed-pattern-noise.md +104 -0
- package/docs/api/sensor-iso-gain.md +63 -0
- package/docs/api/sensor-iso-signal-chain.md +13 -22
- package/docs/api/sensor-local-exposure-binding.md +12 -13
- package/docs/api/sensor-optical-stack.md +37 -40
- package/docs/api/sensor-photon-energy.md +10 -11
- package/docs/api/sensor-physical-charge-capacity.md +8 -13
- package/docs/api/sensor-pixel-pitch.md +1 -1
- package/docs/api/sensor-radiometry-readiness.md +9 -9
- package/docs/api/sensor-raw-readout.md +118 -52
- package/docs/api/sensor-raw-reconstruction.md +97 -20
- package/docs/api/sensor-readout-exposure-linkage.md +120 -24
- package/docs/api/sensor-readout-timing.md +33 -35
- package/docs/api/sensor-response-application-compatibility.md +9 -11
- package/docs/api/sensor-response-operating-range.md +14 -18
- package/docs/api/sensor-responsivity-photocurrent.md +9 -12
- package/docs/api/sensor-sampling-aperture.md +22 -25
- package/docs/api/sensor-scene-eqe-temporal-exposure.md +9 -8
- package/docs/api/sensor-sensor-geometry.md +23 -24
- package/docs/api/sensor-signal-noise.md +10 -10
- package/docs/api/sensor-spatial-sample-reduction.md +27 -30
- package/docs/api/sensor-spatial-sampling-quadrature.md +37 -39
- package/docs/api/sensor-spatio-spectral-reduction.md +27 -29
- package/docs/api/sensor-spectral-coverage.md +5 -5
- package/docs/api/sensor-spectral-quadrature.md +33 -35
- package/docs/api/sensor-spectral-response.md +28 -16
- package/docs/api/sensor-temporal-photo-signal.md +6 -6
- package/docs/api/simulation-poc-simulation.md +17 -16
- package/docs/api/stabilization-camera-shake.md +29 -10
- package/docs/api/stabilization-system.md +25 -21
- package/docs/examples/production-request.json +3821 -2157
- package/docs/examples/simulate-capture.mjs +14 -0
- package/llms.txt +35 -0
- package/package.json +32 -11
- package/AGENTS.md +0 -674
- package/CONTRIBUTING.md +0 -76
- package/DCO +0 -33
- package/dist/camera/equivalent-focal-length.d.ts.map +0 -1
- package/dist/camera/field-of-view.d.ts.map +0 -1
- package/dist/camera/projected-object-size.d.ts.map +0 -1
- package/dist/capture/capture-export-metadata.d.ts.map +0 -1
- package/dist/capture/environment-raw-producer.d.ts.map +0 -1
- package/dist/capture/export-jpeg.d.ts.map +0 -1
- package/dist/capture/export-tiff.d.ts.map +0 -1
- package/dist/capture/focus-control.d.ts.map +0 -1
- package/dist/capture/linear-encoding.d.ts.map +0 -1
- package/dist/capture/native-environment-raw.d.ts.map +0 -1
- package/dist/capture/native-raw-development.d.ts.map +0 -1
- package/dist/capture/native-raw.d.ts.map +0 -1
- package/dist/capture/photographic-export.d.ts.map +0 -1
- package/dist/capture/raw-frame-limits.d.ts.map +0 -1
- package/dist/capture/raw-frame-reconstruction.d.ts.map +0 -1
- package/dist/capture/release-sequence.d.ts.map +0 -1
- package/dist/capture/sensor-raw-frame.d.ts.map +0 -1
- package/dist/capture/sensor-raw-producer.d.ts.map +0 -1
- package/dist/capture/simulated-capture.d.ts.map +0 -1
- package/dist/color/capture-color.d.ts.map +0 -1
- package/dist/color/sensor-color-development.d.ts.map +0 -1
- package/dist/color/srgb-icc.d.ts.map +0 -1
- package/dist/color/srgb-transfer.d.ts.map +0 -1
- package/dist/color/white-balance.d.ts.map +0 -1
- package/dist/composition/image-formation-plan.d.ts.map +0 -1
- package/dist/composition/plan-consumer.d.ts.map +0 -1
- package/dist/core/calculation-result.d.ts.map +0 -1
- package/dist/core/canonical-json.d.ts.map +0 -1
- package/dist/core/configuration-error.d.ts.map +0 -1
- package/dist/core/evidence-provenance.d.ts.map +0 -1
- package/dist/core/image-formation.d.ts.map +0 -1
- package/dist/core/owned-data.d.ts.map +0 -1
- package/dist/core/record-validation.d.ts.map +0 -1
- package/dist/core/scientific-assurance.d.ts.map +0 -1
- package/dist/core/spectral-composition.d.ts.map +0 -1
- package/dist/core/spectral.d.ts.map +0 -1
- package/dist/core/validation.d.ts.map +0 -1
- package/dist/core/version.d.ts.map +0 -1
- package/dist/equipment/exposure-capabilities.d.ts.map +0 -1
- package/dist/equipment/generic-tier-assets.d.ts.map +0 -1
- package/dist/equipment/generic-tier-presets.d.ts.map +0 -1
- package/dist/equipment/iso-capabilities.d.ts.map +0 -1
- package/dist/equipment/metering-capabilities.d.ts.map +0 -1
- package/dist/equipment/release-capabilities.d.ts.map +0 -1
- package/dist/exposure/exposure-duration-control.d.ts.map +0 -1
- package/dist/exposure/exposure-mode-resolver.d.ts.map +0 -1
- package/dist/exposure/exposure.d.ts.map +0 -1
- package/dist/exposure/flash.d.ts.map +0 -1
- package/dist/exposure/metering-scene-radiance.d.ts.map +0 -1
- package/dist/exposure/metering-target.d.ts.map +0 -1
- package/dist/exposure/metering-temporal.d.ts.map +0 -1
- package/dist/exposure/metering.d.ts.map +0 -1
- package/dist/exposure/program-line.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/motion/camera-rotation.d.ts.map +0 -1
- package/dist/motion/capture-rotation-inverse-mapping.d.ts.map +0 -1
- package/dist/motion/capture-rotation-temporal-quadrature.d.ts.map +0 -1
- package/dist/motion/capture-rotation-trajectory.d.ts.map +0 -1
- package/dist/motion/capture-translation-parallax-temporal-quadrature.d.ts.map +0 -1
- package/dist/motion/extended-object-projection.d.ts.map +0 -1
- package/dist/motion/projected-motion.d.ts.map +0 -1
- package/dist/optics/aperture.d.ts.map +0 -1
- package/dist/optics/circle-of-confusion.d.ts.map +0 -1
- package/dist/optics/complex-pupil-psf.d.ts.map +0 -1
- package/dist/optics/depth-of-field.d.ts.map +0 -1
- package/dist/optics/diffraction.d.ts.map +0 -1
- package/dist/optics/focus-breathing.d.ts.map +0 -1
- package/dist/optics/focus-state.d.ts.map +0 -1
- package/dist/optics/front-of-lens-filter.d.ts.map +0 -1
- package/dist/optics/illumination-vignetting.d.ts.map +0 -1
- package/dist/optics/lateral-chromatic-aberration.d.ts.map +0 -1
- package/dist/optics/lens-psf-profile.d.ts.map +0 -1
- package/dist/optics/polygon-diffraction.d.ts.map +0 -1
- package/dist/optics/profile-contract.d.ts.map +0 -1
- package/dist/optics/psf-foundation.d.ts.map +0 -1
- package/dist/optics/radial-distortion.d.ts.map +0 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts.map +0 -1
- package/dist/optics/scene-to-sensor-quadrature.d.ts.map +0 -1
- package/dist/optics/sensor-aperture-rays.d.ts.map +0 -1
- package/dist/optics/sensor-environment-query.d.ts.map +0 -1
- package/dist/optics/sensor-psf-quadrature.d.ts.map +0 -1
- package/dist/optics/sensor-psf-support.d.ts.map +0 -1
- package/dist/optics/stray-light.d.ts.map +0 -1
- package/dist/optics/thin-lens.d.ts.map +0 -1
- package/dist/output/capture-corrected-sdr.d.ts.map +0 -1
- package/dist/output/capture-geometry.d.ts.map +0 -1
- package/dist/output/capture-sdr.d.ts.map +0 -1
- package/dist/output/crop.d.ts.map +0 -1
- package/dist/output/geometric-transforms.d.ts.map +0 -1
- package/dist/output/lens-corrections.d.ts.map +0 -1
- package/dist/output/native-capture-sdr.d.ts.map +0 -1
- package/dist/output/print-contrast-reference.d.ts.map +0 -1
- package/dist/output/print-detail.d.ts.map +0 -1
- package/dist/output/print-jpeg.d.ts.map +0 -1
- package/dist/output/print-plan.d.ts.map +0 -1
- package/dist/output/print-region-statistics.d.ts.map +0 -1
- package/dist/output/sdr-rendering.d.ts.map +0 -1
- package/dist/output/subject-framing-crop.d.ts.map +0 -1
- package/dist/schema/camera.d.ts.map +0 -1
- package/dist/schema/camera.js +0 -3
- package/dist/schema/camera.js.map +0 -1
- package/dist/schema/illumination-spectral.d.ts.map +0 -1
- package/dist/schema/illumination-temporal.d.ts.map +0 -1
- package/dist/schema/illumination.d.ts.map +0 -1
- package/dist/schema/scene-radiance-temporal.d.ts.map +0 -1
- package/dist/schema/scene-radiance.d.ts.map +0 -1
- package/dist/schema/scene.d.ts.map +0 -1
- package/dist/schema/scene.js +0 -3
- package/dist/schema/scene.js.map +0 -1
- package/dist/schema/validation.d.ts.map +0 -1
- package/dist/sensor/accumulated-charge.d.ts.map +0 -1
- package/dist/sensor/architecture.d.ts.map +0 -1
- package/dist/sensor/camera-saturation-capacity.d.ts.map +0 -1
- package/dist/sensor/capture-color-sampling-binding.d.ts.map +0 -1
- package/dist/sensor/capture-mode-timing.d.ts.map +0 -1
- package/dist/sensor/capture-mode.d.ts.map +0 -1
- package/dist/sensor/color-sampling.d.ts.map +0 -1
- package/dist/sensor/constant-rate-temporal-integration.d.ts.map +0 -1
- package/dist/sensor/dark-current.d.ts.map +0 -1
- package/dist/sensor/environment-photo-signal.d.ts.map +0 -1
- package/dist/sensor/eqe-electron-rate.d.ts.map +0 -1
- package/dist/sensor/eqe-local-exposure.d.ts.map +0 -1
- package/dist/sensor/eqe-temporal-exposure.d.ts.map +0 -1
- package/dist/sensor/exposure-window.d.ts.map +0 -1
- package/dist/sensor/iso-signal-chain.d.ts.map +0 -1
- package/dist/sensor/local-exposure-binding.d.ts.map +0 -1
- package/dist/sensor/optical-stack.d.ts.map +0 -1
- package/dist/sensor/photon-energy.d.ts.map +0 -1
- package/dist/sensor/physical-charge-capacity.d.ts.map +0 -1
- package/dist/sensor/pixel-pitch.d.ts.map +0 -1
- package/dist/sensor/radiometry-readiness.d.ts.map +0 -1
- package/dist/sensor/raw-readout.d.ts.map +0 -1
- package/dist/sensor/raw-reconstruction.d.ts.map +0 -1
- package/dist/sensor/readout-exposure-linkage.d.ts.map +0 -1
- package/dist/sensor/readout-timing.d.ts.map +0 -1
- package/dist/sensor/response-application-compatibility.d.ts.map +0 -1
- package/dist/sensor/response-operating-range.d.ts.map +0 -1
- package/dist/sensor/responsivity-photocurrent.d.ts.map +0 -1
- package/dist/sensor/sampling-aperture.d.ts.map +0 -1
- package/dist/sensor/scene-eqe-temporal-exposure.d.ts.map +0 -1
- package/dist/sensor/sensor-geometry.d.ts.map +0 -1
- package/dist/sensor/signal-noise.d.ts.map +0 -1
- package/dist/sensor/spatial-sample-reduction.d.ts.map +0 -1
- package/dist/sensor/spatial-sampling-quadrature.d.ts.map +0 -1
- package/dist/sensor/spatio-spectral-reduction.d.ts.map +0 -1
- package/dist/sensor/spectral-coverage.d.ts.map +0 -1
- package/dist/sensor/spectral-quadrature.d.ts.map +0 -1
- package/dist/sensor/spectral-response.d.ts.map +0 -1
- package/dist/sensor/temporal-eqe-sum.d.ts.map +0 -1
- package/dist/sensor/temporal-photo-signal.d.ts.map +0 -1
- package/dist/sensor/temporal-sample-binding.d.ts.map +0 -1
- package/dist/simulation/poc-simulation.d.ts.map +0 -1
- package/dist/stabilization/camera-shake.d.ts.map +0 -1
- package/dist/stabilization/system.d.ts.map +0 -1
- package/docs/API_STYLE.md +0 -235
- package/docs/ARCHITECTURE.md +0 -356
- package/docs/AUTOMATION_COORDINATION.md +0 -190
- package/docs/CAPTURE_COLOR.md +0 -25
- package/docs/CAPTURE_CORRECTED_SDR.md +0 -25
- package/docs/CAPTURE_EXPORT_METADATA.md +0 -27
- package/docs/CAPTURE_SDR.md +0 -22
- package/docs/DENSE_PUPIL_QUALIFICATION.md +0 -121
- package/docs/DEVELOPERS.md +0 -68
- package/docs/DIGITAL_OPTICS_FOUNDATION.md +0 -69
- package/docs/ENVIRONMENT_RAW_CAPTURE.md +0 -208
- package/docs/EXPORT_ACCEPTANCE.md +0 -133
- package/docs/GENERIC_TIER_PRESETS.md +0 -269
- package/docs/GEOMETRIC_PREPARATION_REUSE.md +0 -141
- package/docs/IMAGE_FORMATION.md +0 -247
- package/docs/LINEAR_CAPTURE_ENCODING.md +0 -25
- package/docs/MOTION_AND_SIGNAL.md +0 -250
- package/docs/NATIVE_CAPTURE_SDR.md +0 -70
- package/docs/NATIVE_CAPTURE_SDR_REVIEW.md +0 -37
- package/docs/NATIVE_CAPTURE_SDR_VALIDATION.md +0 -34
- package/docs/NATIVE_RAW_AND_PRINT.md +0 -54
- package/docs/NUMERICAL_CORRECTNESS.md +0 -191
- package/docs/PERFORMANCE_BASELINE.md +0 -56
- package/docs/PHOTOGRAPHIC_EXPORT.md +0 -206
- package/docs/PHYSICS_FOUNDATION.md +0 -426
- package/docs/POC_API.md +0 -278
- package/docs/POC_PROJECTION_PLATFORM_REFERENCE.md +0 -12
- package/docs/POC_PROJECTION_REUSE.md +0 -124
- package/docs/PRINT_195_196_REVIEW.md +0 -93
- package/docs/PRINT_196_ENGINE_ACCEPTANCE.md +0 -45
- package/docs/PRINT_CONTRAST_REFERENCE.md +0 -43
- package/docs/PRINT_DETAIL_ASSESSMENT.md +0 -81
- package/docs/PRINT_DETAIL_JPEG_EVIDENCE.md +0 -36
- package/docs/PRINT_DETAIL_SDR_EVIDENCE.md +0 -29
- package/docs/PRINT_DETAIL_VALIDATION.md +0 -36
- package/docs/PRINT_PLANNING.md +0 -75
- package/docs/PRINT_REGION_STATISTICS.md +0 -38
- package/docs/PROCESSED_OUTPUT.md +0 -157
- package/docs/PRODUCTION_COMPOSITION.md +0 -373
- package/docs/PRODUCTION_ENVIRONMENT_CAPTURE.md +0 -131
- package/docs/PROVENANCE.md +0 -115
- package/docs/PSF_FOUNDATION.md +0 -165
- package/docs/RAW_FRAME_ENVELOPE.md +0 -56
- package/docs/RAW_FRAME_RECONSTRUCTION.md +0 -30
- package/docs/RELEASE_1_0.md +0 -66
- package/docs/RELEASE_1_0_1.md +0 -61
- package/docs/RELEASE_1_1_0.md +0 -25
- package/docs/RELEASE_1_2_0.md +0 -51
- package/docs/RELEASE_1_3_0.md +0 -15
- package/docs/RELEASE_1_4_0.md +0 -23
- package/docs/RELEASE_1_5_0.md +0 -105
- package/docs/RELEASE_AUDIT.md +0 -39
- package/docs/SCENE_RADIANCE_AND_ILLUMINATION.md +0 -298
- package/docs/SCENE_SENSOR_QUADRATURE.md +0 -98
- package/docs/SCENE_SENSOR_TEMPORAL_EXPOSURE.md +0 -86
- package/docs/SCIENTIFIC_ASSURANCE.md +0 -280
- package/docs/SCIENTIFIC_CONFORMANCE.md +0 -83
- package/docs/SDR_RENDERING.md +0 -33
- package/docs/SENSOR_APERTURE_RAYS.md +0 -23
- package/docs/SENSOR_ENVIRONMENT_QUERY.md +0 -86
- package/docs/SENSOR_EQE_LOCAL_EXPOSURE.md +0 -104
- package/docs/SENSOR_EQE_TEMPORAL_EXPOSURE.md +0 -74
- package/docs/SENSOR_PSF_QUADRATURE.md +0 -90
- package/docs/SENSOR_RAW_FRAME.md +0 -20
- package/docs/SENSOR_RAW_PRODUCER.md +0 -77
- package/docs/SEPARABLE_EMISSION_DRAFT.md +0 -103
- package/docs/SIGNAL_CONFORMANCE.md +0 -23
- package/docs/SIMULATED_CAPTURE.md +0 -65
- package/docs/STABILIZATION.md +0 -234
- package/docs/TEMPORAL_PHOTO_RAW.md +0 -83
- package/docs/TIER_ACCEPTANCE.md +0 -129
- package/docs/TIER_PRESET_ACCEPTANCE.md +0 -59
- package/docs/V1_COMPOSITION_MAP.md +0 -123
- package/docs/V1_HELPER_REUSE_REVIEW.md +0 -60
- package/docs/V1_PERFORMANCE_DISPOSITION.md +0 -88
- package/docs/benchmarks/2026-10-01-main-baseline.json +0 -694
- package/docs/benchmarks/2026-10-02-geometric-preparation-reuse.json +0 -2190
- package/docs/benchmarks/2026-10-02-poc-projection-reuse.json +0 -2423
- package/docs/benchmarks/2026-10-02-v1-environment-gc-profile.json +0 -210
- package/docs/benchmarks/2026-10-02-v1-environment-ordinary.json +0 -187
- package/docs/benchmarks/2026-10-02-v1-geometry-gc-profile.json +0 -756
- package/docs/benchmarks/2026-10-02-v1-geometry-ordinary-1.json +0 -711
- package/docs/benchmarks/2026-10-02-v1-geometry-ordinary-2.json +0 -711
- package/docs/benchmarks/2026-10-02-v1-poc-gc-profile.json +0 -885
- package/docs/benchmarks/2026-10-02-v1-poc-ordinary-1.json +0 -868
- package/docs/benchmarks/2026-10-02-v1-poc-ordinary-2.json +0 -868
- package/docs/licenses/castleCSF-MIT.txt +0 -21
- package/docs/validation/dense-pupil-2026-10-06/pilot.jsonl +0 -6
- package/docs/validation/documentation-fragments.json +0 -212
- package/docs/validation/native-capture-sdr-darwin-arm64-node26-review.jsonl +0 -4
- package/docs/validation/native-capture-sdr-darwin-arm64.jsonl +0 -3
- package/docs/validation/native-raw-print-2026-10-04/fractional-asymmetric-crop.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/gray-ramp.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/linear-area-checker.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/manifest.json +0 -218
- package/docs/validation/native-raw-print-2026-10-04/megapixel-native.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/native-color.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel-validation.json +0 -9
- package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json +0 -29
- package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.codes.u16le.gz +0 -0
- package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.jpg +0 -0
- package/docs/validation/native-raw-print-2026-10-04/photivra-native-full-megapixel.json.rgb.gz +0 -0
- package/docs/validation/native-raw-print-2026-10-04/srgb.icc +0 -0
- package/docs/validation/native-raw-print-2026-10-04/validation.json +0 -41
- package/docs/validation/print-196-candidate-capabilities.json +0 -28
- package/docs/validation/print-196-candidate-evidence.json +0 -356
- package/docs/validation/print-196-engine-acceptance.json +0 -81
- package/docs/validation/print-detail-capabilities.json +0 -103
- package/docs/validation/print-detail-jpeg-evidence.json +0 -116
- package/docs/validation/print-detail-sdr-evidence.json +0 -81
- package/docs/validation/release-audit-inventory.json +0 -1306
- package/docs/validation/tier-acceptance-report-v1.json +0 -2538
|
@@ -0,0 +1,1281 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* `simulateCapture()` is the easy starting point. It turns a small input into a complete native
|
|
4
|
+
* environment RAW request: scene radiance → optics → spectral EQE (external quantum efficiency) →
|
|
5
|
+
* charge → readout and ADC. That request then runs through the existing native RAW development task
|
|
6
|
+
* and, optionally, the existing JPEG encoder.
|
|
7
|
+
*
|
|
8
|
+
* It adds no physics of its own. Every number comes from existing public contracts: generic tier
|
|
9
|
+
* presets, exposure metering and mode resolution, colorimetry, native environment RAW, native RAW
|
|
10
|
+
* development and print JPEG.
|
|
11
|
+
*
|
|
12
|
+
* Everything the facade decides for the caller is listed in `result.assumptions`: defaults, derived
|
|
13
|
+
* values and fixed modelling choices. Nothing is hidden. The complete underlying request is returned
|
|
14
|
+
* in `result.request`; get the per-site inputs with `request.siteInput(index)`.
|
|
15
|
+
*
|
|
16
|
+
* The 2.0 options are opt-in and only combine existing public contracts. If none is given, the
|
|
17
|
+
* request, RAW, samples and JPEG are byte-identical to the 1.0 facade:
|
|
18
|
+
* - output formats: `png` (`serializePngImage`; sRGB, or Display P3 / Rec. 2020 through the colorimetry
|
|
19
|
+
* matrices and transfer functions) and `dng`
|
|
20
|
+
* (`serializeNativeRawDng` of the packed native RAW; no linear master is declared);
|
|
21
|
+
* - demosaic: `createBuiltInBayerDemosaicProfiles` (bilinear) and mirror borders;
|
|
22
|
+
* - optics: the environment `lensFieldMapping` (projection, radial / Brown–Conrady distortion), radial
|
|
23
|
+
* illumination vignetting as field throughput and a caller sampled lens PSF;
|
|
24
|
+
* - motion: constant camera angular velocity and the electronic rolling-shutter exposure schedule.
|
|
25
|
+
* The 2.1 options are opt-in in the same way: `lens.diffraction`, `output.demosaic: "ahd"` and
|
|
26
|
+
* `sensor.antiAliasing` (a birefringent point-splitting low-pass filter in the sensor optical stack).
|
|
27
|
+
* @see docs/QUICK_START.md
|
|
28
|
+
*/
|
|
29
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
30
|
+
import { requireInputObject } from "../core/record-validation.js";
|
|
31
|
+
import { ENGINE_API_VERSION } from "../core/version.js";
|
|
32
|
+
import { GENERIC_TIER_ASSETS } from "../equipment/generic-tier-assets.js";
|
|
33
|
+
import { parseCallerCameraPreset } from "./camera-preset.js";
|
|
34
|
+
import { createHostYield } from "./host-yield.js";
|
|
35
|
+
import { resolveGenericEquipmentExposureCapabilities } from "../equipment/exposure-capabilities.js";
|
|
36
|
+
import { resolveGenericIsoSignalChain } from "../sensor/iso-signal-chain.js";
|
|
37
|
+
import { meterRelativeExposure } from "../exposure/metering.js";
|
|
38
|
+
import { createExposureMeterTargetFromMeteringResult, setExposureCompensationOnMeterTarget } from "../exposure/metering-target.js";
|
|
39
|
+
import { resolveManualExposureMode, resolveAperturePriorityExposureMode, resolveShutterPriorityExposureMode, resolveProgramAutoExposureMode } from "../exposure/exposure-mode-resolver.js";
|
|
40
|
+
import { parseExposureProgramLineProfile } from "../exposure/program-line.js";
|
|
41
|
+
import { resolveCaptureGeometry } from "../output/capture-geometry.js";
|
|
42
|
+
import { estimateCie1931ColorMatchingFunctions } from "../color/color-matching-functions.js";
|
|
43
|
+
import { createColorTemperatureWhiteBalanceIntent, resolveColorTemperatureWhiteBalance, resolveCustomWhiteBalance, resolveManualWhiteBalance, estimateAutoWhiteBalance } from "../color/white-balance.js";
|
|
44
|
+
import { createCaptureWhiteBalanceIntent } from "../capture/simulated-capture.js";
|
|
45
|
+
import { SENSOR_RAW_PRODUCER_NOISE_MODEL } from "../capture/sensor-raw-producer.js";
|
|
46
|
+
import { calculateNativeRawPlan } from "../capture/native-raw.js";
|
|
47
|
+
import { createNativeEnvironmentRawTask } from "../capture/native-environment-raw.js";
|
|
48
|
+
import { createNativeRawDevelopmentTask } from "../capture/native-raw-development.js";
|
|
49
|
+
import { parseSdrRenderingProfile } from "../output/sdr-rendering.js";
|
|
50
|
+
import { createPrintJpegTask } from "../output/print-jpeg.js";
|
|
51
|
+
import { checkNativeRawIntegrity, prepareNativeRawDevelopment } from "../capture/native-raw-development.js";
|
|
52
|
+
import { createRawPixelDeveloper, normalizedRawSite, rawSiteSaturated } from "../capture/raw-development-kernel.js";
|
|
53
|
+
import { createAhdDemosaicTask } from "../processing/demosaic.js";
|
|
54
|
+
import { renderSdrRunInto } from "../output/native-capture-sdr.js";
|
|
55
|
+
import { prepareSdrPixelTone } from "../output/sdr-rendering.js";
|
|
56
|
+
import { BUILT_IN_BAYER_DEMOSAIC_MODEL_VERSION as BUILT_IN_DEMOSAIC_VERSION, createBuiltInBayerDemosaicProfiles } from "../sensor/raw-reconstruction.js";
|
|
57
|
+
import { transformOrientedRasterPointToNative } from "../output/capture-geometry.js";
|
|
58
|
+
import { resolveCaptureColorModel } from "../color/capture-color.js";
|
|
59
|
+
import { evaluateBt709Oetf, evaluateSrgbEncoding, resolveRgbColorSpace } from "../color/rgb-color-spaces.js";
|
|
60
|
+
import { serializePngImage } from "../output/png-encoder.js";
|
|
61
|
+
import { parseLensProjectionModel } from "../camera/lens-projection.js";
|
|
62
|
+
import { prepareEnvironmentLensFieldMapping } from "../optics/sensor-environment-query.js";
|
|
63
|
+
import { createRollingShutterCaptureExposureSchedule, resolveElectronicRollingShutterExposureSchedule } from "../sensor/readout-exposure-linkage.js";
|
|
64
|
+
import { calculateCaptureExposureWindows } from "../sensor/exposure-window.js";
|
|
65
|
+
import { calculateSensorSpectralQuadrature } from "../sensor/spectral-quadrature.js";
|
|
66
|
+
import { createDiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
67
|
+
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
68
|
+
import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
69
|
+
import { multiplyMatrices } from "../color/colorimetry-math.js";
|
|
70
|
+
import { prepareRgbGamutMapping } from "../color/gamut-mapping.js";
|
|
71
|
+
import { serializeNativeRawDng } from "../capture/native-raw-dng.js";
|
|
72
|
+
import { resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
73
|
+
import { resolveChartScene, CHART_SCENE_PRESETS } from "./chart-scenes.js";
|
|
74
|
+
import { resolveCaptureIlluminant, calculateIlluminantIrradianceScale, buildPlanarSceneRadianceProvider, requireCallerScene, FACADE_SPECTRAL_RANGE_NANOMETERS, PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W } from "./scene-radiance.js";
|
|
75
|
+
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
76
|
+
export const SIMULATE_CAPTURE_FACADE_VERSION = "1.0.0";
|
|
77
|
+
/**
|
|
78
|
+
* Options reported in `diagnostics.options` (2.1), in this order, when they ran: the ones that change how much work
|
|
79
|
+
* a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.temporalSampleCount` a count above 1.
|
|
80
|
+
*/
|
|
81
|
+
export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
82
|
+
"lens.diffraction", "sensor.antiAliasing", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror",
|
|
83
|
+
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
84
|
+
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
85
|
+
const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
86
|
+
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
87
|
+
const DEFAULTS = Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
88
|
+
subjectDistanceM: 5, width: 64, height: 64, format: "srgb-8", toneCurve: "identity", highlightMode: "clip",
|
|
89
|
+
whiteBalance: "scene-illuminant", jpegQuantizationStep: 2, pixelPitchMicrometers: 6, spectralStepNanometers: 20,
|
|
90
|
+
temporalSampleCount: 1, noiseSeed: 1, illuminantKelvin: 6500 });
|
|
91
|
+
/** Standard output sensitivity S = 10 / H (CIPA DC-004-2020 §3.2): the reference exposure in lx·s is 10 / S. */
|
|
92
|
+
const SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS = 10;
|
|
93
|
+
/** Camera equation H = 0.65·L·t/N² (CIPA DC-004-2020 §5.5 (4); 0.65 = τ·π/4 with τ = 0.828, Explanation 4). */
|
|
94
|
+
const CAMERA_EQUATION_CONSTANT = 0.65;
|
|
95
|
+
/** Neutral reference reflectance: DC-004 ties the SOS reference level to an 18 % grey (Explanation 1 and 2.2). */
|
|
96
|
+
const GREY_CARD_REFLECTANCE = 0.18;
|
|
97
|
+
/** Scene luminance (cd/m²) whose focal-plane exposure 0.65·L·t/N² equals the SOS reference exposure 10/S. */
|
|
98
|
+
function sosReferenceLuminance(fNumber, shutterSeconds, iso) {
|
|
99
|
+
return SOS_REFERENCE_EXPOSURE_NUMERATOR_LUX_SECONDS * fNumber ** 2 / (CAMERA_EQUATION_CONSTANT * shutterSeconds * iso);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
103
|
+
* output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
|
|
104
|
+
* E = π·10·N²/(0.65·t·S·0.18) = π·80 000/11.7 ≈ 21 480.98 lux.
|
|
105
|
+
*/
|
|
106
|
+
export const SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX = Math.PI * sosReferenceLuminance(DEFAULTS.fNumber, DEFAULTS.shutterSeconds, DEFAULTS.iso) /
|
|
107
|
+
GREY_CARD_REFLECTANCE;
|
|
108
|
+
/** Demosaic border, native sites per side. */
|
|
109
|
+
const MARGIN = 1;
|
|
110
|
+
/**
|
|
111
|
+
* AHD border, native sites per side: an AHD output pixel depends on sites up to 5 away (directional green ±2,
|
|
112
|
+
* colour differences ±1 of it, the homogeneity's 4-neighbours and its 3 × 3 sum), so a 5-site rendered margin keeps
|
|
113
|
+
* every output pixel off the plane's mirrored edge.
|
|
114
|
+
*/
|
|
115
|
+
const AHD_MARGIN = 5;
|
|
116
|
+
/** Sites whose values enter an AHD output pixel (interpolation reach), for the saturated-pixel count. */
|
|
117
|
+
const AHD_VALUE_REACH = 3;
|
|
118
|
+
/** Anti-aliasing filter limits: split points, and the largest offset in sites (bounds the scene work and the convolution margin). */
|
|
119
|
+
const AA_MAXIMUM_COMPONENTS = 16;
|
|
120
|
+
const AA_MAXIMUM_OFFSET_SITES = 4;
|
|
121
|
+
/** Default birefringent separation in sites: the two-spot MTF |cos(π·f·d)| then has its first zero at Nyquist, f = 1/(2·pitch). */
|
|
122
|
+
const AA_DEFAULT_SEPARATION_SITES = 1;
|
|
123
|
+
/** Parses `sensor.antiAliasing` into engine point-splitting components (offsets in µm, +x right, +y down). */
|
|
124
|
+
function parseAntiAliasing(value, pitch) {
|
|
125
|
+
requireInputObject(value, "sensor.antiAliasing");
|
|
126
|
+
const kind = value.kind;
|
|
127
|
+
const separation = (record) => {
|
|
128
|
+
if (record.separationSites === undefined)
|
|
129
|
+
return AA_DEFAULT_SEPARATION_SITES;
|
|
130
|
+
const d = positive(record.separationSites, "sensor.antiAliasing.separationSites");
|
|
131
|
+
if (d > AA_MAXIMUM_OFFSET_SITES)
|
|
132
|
+
fail("sensor.antiAliasing.separationSites must be greater than 0 and at most " + AA_MAXIMUM_OFFSET_SITES + ".");
|
|
133
|
+
return d;
|
|
134
|
+
};
|
|
135
|
+
if (kind === "four-spot") {
|
|
136
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
137
|
+
return { kind, separationSites: d, components: [[-h, -h], [h, -h], [-h, h], [h, h]].map(([x, y]) => ({ offsetMicrometers: { x: x, y: y }, normalizedWeight: 0.25 })) };
|
|
138
|
+
}
|
|
139
|
+
if (kind === "two-spot") {
|
|
140
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "axis", "separationSites"]), d = separation(record), h = d * pitch / 2;
|
|
141
|
+
if (record.axis !== "horizontal" && record.axis !== "vertical")
|
|
142
|
+
fail("sensor.antiAliasing.axis must be horizontal or vertical.");
|
|
143
|
+
const axis = record.axis;
|
|
144
|
+
return { kind, axis, separationSites: d, components: [-h, h].map((o) => ({ offsetMicrometers: axis === "horizontal" ? { x: o, y: 0 } : { x: 0, y: o }, normalizedWeight: 0.5 })) };
|
|
145
|
+
}
|
|
146
|
+
if (kind === "point-splitting") {
|
|
147
|
+
const record = allowKeys(value, "sensor.antiAliasing", ["kind", "components"]);
|
|
148
|
+
if (!Array.isArray(record.components) || record.components.length === 0 || record.components.length > AA_MAXIMUM_COMPONENTS) {
|
|
149
|
+
fail("sensor.antiAliasing.components must be an array of 1 to " + AA_MAXIMUM_COMPONENTS + " components.");
|
|
150
|
+
}
|
|
151
|
+
// The engine's optical-stack parser checks shape, finiteness, positive weights and the unit sum (1e-12).
|
|
152
|
+
const kernel = resolveAntiAliasingSpatialKernel({ schemaVersion: "0.1.0", profileId: "facade-aa-check", evidence: FACADE_EVIDENCE,
|
|
153
|
+
effectiveAntiAliasingSpatialResponse: { kind: "normalized-point-splitting-kernel", evidence: FACADE_EVIDENCE, coordinateSystem: "native-sensor-physical",
|
|
154
|
+
scope: "field-wavelength-polarization-invariant-approximation", components: record.components } });
|
|
155
|
+
for (const c of kernel.components)
|
|
156
|
+
if (Math.max(Math.abs(c.offsetMicrometers.x), Math.abs(c.offsetMicrometers.y)) > AA_MAXIMUM_OFFSET_SITES * pitch) {
|
|
157
|
+
fail("sensor.antiAliasing.components offsets must lie within " + AA_MAXIMUM_OFFSET_SITES + " sites (" + AA_MAXIMUM_OFFSET_SITES * pitch + " µm) of the site centre.");
|
|
158
|
+
}
|
|
159
|
+
return { kind, components: kernel.components.map((c) => ({ offsetMicrometers: { ...c.offsetMicrometers }, normalizedWeight: c.normalizedWeight })) };
|
|
160
|
+
}
|
|
161
|
+
return fail("sensor.antiAliasing.kind must be four-spot, two-spot or point-splitting.");
|
|
162
|
+
}
|
|
163
|
+
/** Third dark ring of the Airy pattern in units of λ·N_w (j₁,₃/π, j₁,₃ = 10.1734681351): the default diffraction kernel reach. */
|
|
164
|
+
const AIRY_THIRD_ZERO_PER_LAMBDA_N = 10.173468135062722 / Math.PI;
|
|
165
|
+
/** Default temporal midpoints when the camera rotates (2.0 motion option). */
|
|
166
|
+
const ROTATING_TEMPORAL_SAMPLE_COUNT = 8;
|
|
167
|
+
const IDS = Object.freeze({ frame: "facade-frame", mode: "native-still", cfa: "photivra-generic-bayer", binding: "facade-native-cfa",
|
|
168
|
+
sceneState: "facade-scene-state", scene: "facade-scene", provider: "facade-provider", lights: "facade-lights", materials: "facade-materials",
|
|
169
|
+
capture: "facade-capture", spectral: "facade-colorimetric-eqe", sampling: "facade-sampling", stack: "facade-no-aa", stackAa: "facade-aa",
|
|
170
|
+
stationarity: "facade-stationarity", operatingState: "facade-operating-state", dark: "facade-dark", completeness: "facade-completeness",
|
|
171
|
+
chargeSampling: "facade-charge-sampling", capacity: "facade-capacity" });
|
|
172
|
+
const CHANNELS = ["red", "green", "blue"];
|
|
173
|
+
/**
|
|
174
|
+
* Synthetic colorimetric sensor: channel EQE(λ) = a·cmf(λ)·(555 nm/λ), with cmf = x̄, ȳ, z̄. The λ factor turns
|
|
175
|
+
* the energy-weighted CMF (colour-matching function) into a photon-counting response. So channel counts are
|
|
176
|
+
* proportional to CIE XYZ (the Luther–Ives condition holds, up to quadrature error), and the camera→XYZ
|
|
177
|
+
* matrix is diagonal.
|
|
178
|
+
*/
|
|
179
|
+
const EQE_SCALE = Object.freeze({ red: 0.5, green: 0.5, blue: 0.25 });
|
|
180
|
+
const CMF_KEY = Object.freeze({ red: "x", green: "y", blue: "z" });
|
|
181
|
+
const CAMERA_TO_XYZ = Object.freeze([[EQE_SCALE.green / EQE_SCALE.red, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.green / EQE_SCALE.blue]].map((r) => Object.freeze(r)));
|
|
182
|
+
const XYZ_TO_CAMERA = [[EQE_SCALE.red / EQE_SCALE.green, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.blue / EQE_SCALE.green]];
|
|
183
|
+
/** Synthetic dark current per site at the operating temperature, e⁻/s. */
|
|
184
|
+
const DARK_CURRENT_ELECTRONS_PER_SECOND = 1;
|
|
185
|
+
const OPERATING_TEMPERATURE_C = 20;
|
|
186
|
+
function fail(message) { throw new InvalidConfigurationError(message); }
|
|
187
|
+
/** Diagnostic clock for the 2.1 stage timings: `performance.now()` where the host has it. Never affects output. */
|
|
188
|
+
function clockMs() { return globalThis.performance?.now() ?? Date.now(); }
|
|
189
|
+
function positive(value, name) {
|
|
190
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
191
|
+
fail(name + " must be a positive finite number.");
|
|
192
|
+
return value;
|
|
193
|
+
}
|
|
194
|
+
function integerIn(value, name, minimum, maximum) {
|
|
195
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum)
|
|
196
|
+
fail(name + " must be an integer from " + minimum + " to " + maximum + ".");
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
function allowKeys(value, name, keys) {
|
|
200
|
+
if (value === undefined)
|
|
201
|
+
return {};
|
|
202
|
+
requireInputObject(value, name);
|
|
203
|
+
for (const key of Object.keys(value))
|
|
204
|
+
if (!keys.includes(key))
|
|
205
|
+
fail("Unknown " + name + " field: " + key + ".");
|
|
206
|
+
return value;
|
|
207
|
+
}
|
|
208
|
+
function sampled(values) {
|
|
209
|
+
return { wavelengthUnit: "nm", wavelengthBasis: "vacuum", interpolation: "piecewise-linear", outsideRangeBehavior: "fail-closed", evidence: FACADE_EVIDENCE,
|
|
210
|
+
samples: values.map((s) => ({ wavelengthNanometers: s.nm, value: s.value })) };
|
|
211
|
+
}
|
|
212
|
+
async function sha256Hex(text) {
|
|
213
|
+
const subtle = globalThis.crypto?.subtle;
|
|
214
|
+
if (!subtle)
|
|
215
|
+
fail("simulateCapture needs crypto.subtle (a secure browser context or Node 22+) to digest the scene description.");
|
|
216
|
+
const bytes = new Uint8Array(await subtle.digest("SHA-256", new TextEncoder().encode(text)));
|
|
217
|
+
return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* The form in which a caller spectrum enters the scene digest. Tabulated data is hashed exactly as its
|
|
221
|
+
* (wavelength, value) pairs. A function spectrum has no data to hash, so it is sampled at the facade's
|
|
222
|
+
* 1 nm integer nodes over 380-780 nm (the grid the facade also uses to normalize illuminance).
|
|
223
|
+
*/
|
|
224
|
+
function spectrumDigestForm(raw, prepared) {
|
|
225
|
+
const record = raw;
|
|
226
|
+
if (record.kind === "tabulated" && Array.isArray(record.samples)) {
|
|
227
|
+
return { kind: "tabulated", samples: record.samples.map((sample) => [sample.wavelengthNanometers, sample.value]) };
|
|
228
|
+
}
|
|
229
|
+
const values = [];
|
|
230
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm++)
|
|
231
|
+
values.push(prepared.evaluate(nm));
|
|
232
|
+
return { kind: "function-sampled-1nm", rangeNanometers: [FACADE_SPECTRAL_RANGE_NANOMETERS.minimum, FACADE_SPECTRAL_RANGE_NANOMETERS.maximum], values };
|
|
233
|
+
}
|
|
234
|
+
function luminanceOf(radiance) {
|
|
235
|
+
let sum = 0;
|
|
236
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm += 5) {
|
|
237
|
+
const w = nm === FACADE_SPECTRAL_RANGE_NANOMETERS.minimum || nm === FACADE_SPECTRAL_RANGE_NANOMETERS.maximum ? 2.5 : 5;
|
|
238
|
+
sum += w * radiance(nm) * estimateCie1931ColorMatchingFunctions(nm).y;
|
|
239
|
+
}
|
|
240
|
+
return PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W * sum;
|
|
241
|
+
}
|
|
242
|
+
function illuminantXyz(illuminant) {
|
|
243
|
+
let x = 0, y = 0, z = 0;
|
|
244
|
+
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm++) {
|
|
245
|
+
const s = illuminant.shape.evaluate(nm), c = estimateCie1931ColorMatchingFunctions(nm);
|
|
246
|
+
x += s * c.x;
|
|
247
|
+
y += s * c.y;
|
|
248
|
+
z += s * c.z;
|
|
249
|
+
}
|
|
250
|
+
return [x / y, 1, z / y];
|
|
251
|
+
}
|
|
252
|
+
/** Bilinear RGGB demosaic as explicit linear neighbourhood kernels (weights sum to one per output channel). */
|
|
253
|
+
function bilinearPhaseProfiles() {
|
|
254
|
+
const cross = [[1, 0], [-1, 0], [0, 1], [0, -1]], diagonal = [[1, 1], [1, -1], [-1, 1], [-1, -1]], horizontal = [[1, 0], [-1, 0]], vertical = [[0, 1], [0, -1]];
|
|
255
|
+
const average = (offsets, channel) => offsets.map(([x, y]) => ({ offsetX: x, offsetY: y, sourceChannelId: channel, weight: 1 / offsets.length }));
|
|
256
|
+
const self = (channel) => [{ offsetX: 0, offsetY: 0, sourceChannelId: channel, weight: 1 }];
|
|
257
|
+
const kernels = {
|
|
258
|
+
"0,0": { red: self("red"), green: average(cross, "green"), blue: average(diagonal, "blue") },
|
|
259
|
+
"1,0": { red: average(horizontal, "red"), green: self("green"), blue: average(vertical, "blue") },
|
|
260
|
+
"0,1": { red: average(vertical, "red"), green: self("green"), blue: average(horizontal, "blue") },
|
|
261
|
+
"1,1": { red: average(diagonal, "red"), green: average(cross, "green"), blue: self("blue") }
|
|
262
|
+
};
|
|
263
|
+
return [[0, 0], [1, 0], [0, 1], [1, 1]].map(([px, py], index) => ({ phaseX: px, phaseY: py, profile: { schemaVersion: "0.1.0",
|
|
264
|
+
profileId: "facade-bilinear-phase-" + index, profileVersion: "1", captureModeId: IDS.mode, colorSamplingProfileId: IDS.cfa,
|
|
265
|
+
scientificStatus: "approximation", method: "explicit-linear-native-neighborhood",
|
|
266
|
+
normalization: "weights-sum-to-one-per-output-channel", negativeBlackSubtractedValuesAllowed: true,
|
|
267
|
+
kernels: CHANNELS.map((c) => ({ outputChannelId: c, contributions: kernels[px + "," + py][c] })), evidence: FACADE_EVIDENCE,
|
|
268
|
+
limitations: ["Bilinear interpolation of the RGGB mosaic; no edge-directed demosaic, false-colour suppression or calibration."] } }));
|
|
269
|
+
}
|
|
270
|
+
function defaultProgramLine() {
|
|
271
|
+
// Generic educational program line written for the engine, relative to the f/8, 1/125 s reference. For less light
|
|
272
|
+
// it first opens the aperture toward f/4, then lengthens the shutter. For more light it first closes toward
|
|
273
|
+
// f/16, then shortens the shutter.
|
|
274
|
+
return parseExposureProgramLineProfile({ schemaVersion: "0.1.0", profileId: "facade-generic-program-line", profileVersion: "1.0.0",
|
|
275
|
+
scientificStatus: "approximation", policyKind: "generic-program-line", interpolation: "log2-aperture-shutter", evidence: FACADE_EVIDENCE,
|
|
276
|
+
limitations: ["Generic educational program line authored for the capture facade; not any manufacturer's program."],
|
|
277
|
+
nodes: [
|
|
278
|
+
{ nodeId: "minus-6", opticalExposureStopsFromReference: -6, aperture: 16, shutterSeconds: 1 / 2000 },
|
|
279
|
+
{ nodeId: "minus-2", opticalExposureStopsFromReference: -2, aperture: 16, shutterSeconds: 1 / 125 },
|
|
280
|
+
{ nodeId: "zero", opticalExposureStopsFromReference: 0, aperture: 8, shutterSeconds: 1 / 125 },
|
|
281
|
+
{ nodeId: "plus-2", opticalExposureStopsFromReference: 2, aperture: 4, shutterSeconds: 1 / 125 },
|
|
282
|
+
{ nodeId: "plus-9", opticalExposureStopsFromReference: 9, aperture: 4, shutterSeconds: 128 / 125 }
|
|
283
|
+
] });
|
|
284
|
+
}
|
|
285
|
+
/** PNG colour signalling: the sRGB chunk for sRGB, ITU-T H.273 code points through cICP otherwise. */
|
|
286
|
+
function pngColourEncoding(encoding) {
|
|
287
|
+
if (encoding === "srgb")
|
|
288
|
+
return { kind: "srgb", renderingIntent: "perceptual" };
|
|
289
|
+
if (encoding === "display-p3")
|
|
290
|
+
return { kind: "cicp", primaries: "display-p3", transfer: "srgb", fullRange: true };
|
|
291
|
+
return { kind: "cicp", primaries: "bt2020", transfer: "bt709", fullRange: true };
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Develops the packed RAW to unclipped linear sRGB/D65, using the exact operator inside
|
|
295
|
+
* `createNativeRawDevelopmentTask`. Validation, demosaic kernels, border handling, per-site white-level
|
|
296
|
+
* clipping, white balance, highlight mode and the camera→XYZ→linear-sRGB matrices are all the same. It
|
|
297
|
+
* stops before tone mapping, gamut clipping and quantization. It reads the RAW without taking ownership.
|
|
298
|
+
*/
|
|
299
|
+
async function developLinearSrgb(input, afterRow, ahd) {
|
|
300
|
+
const v = prepareNativeRawDevelopment(input), plan = v.plan, raw = input.raw, layout = plan.frame.colorSamplingProfile.layout;
|
|
301
|
+
if (layout.kind !== "periodic-mosaic")
|
|
302
|
+
fail("Development requires a periodic CFA.");
|
|
303
|
+
const native = plan.exposure.geometry.nativeRaster, g = plan.exposure.resolvedGeometry, active = g.activeCapture.nativeRect;
|
|
304
|
+
checkNativeRawIntegrity(raw, native.pixelWidth, layout.repeatWidthSites, layout.repeatHeightSites, 0, native.pixelHeight);
|
|
305
|
+
const developer = createRawPixelDeveloper({ nativeWidth: native.pixelWidth, repeatWidth: layout.repeatWidthSites, repeatHeight: layout.repeatHeightSites,
|
|
306
|
+
codes: raw.codes, blackLevels: raw.blackLevels, saturationFlags: raw.saturationFlags, whiteByPhase: raw.effectiveSaturationCodes }, v.phases, v.development, v.mirror);
|
|
307
|
+
const width = v.raster.pixelWidth, height = v.raster.pixelHeight, rgb = new Float64Array(width * height * 3), row = new Float64Array(width * 3);
|
|
308
|
+
const at = (x, y) => {
|
|
309
|
+
const q = transformOrientedRasterPointToNative({ point: { x: v.crop.x + x + 0.5, y: v.crop.y + y + 0.5 }, nativeRaster: g.activeCapture.raster, orientation: plan.exposure.geometry.orientation });
|
|
310
|
+
return { x: Math.floor(q.x) + active.x, y: Math.floor(q.y) + active.y };
|
|
311
|
+
};
|
|
312
|
+
if (ahd)
|
|
313
|
+
return developAhd(v, raw, layout.repeatWidthSites, layout.repeatHeightSites, width, height, at, afterRow, ahd);
|
|
314
|
+
for (let y = 0; y < height; y++) {
|
|
315
|
+
const first = at(0, y), second = width > 1 ? at(1, y) : first;
|
|
316
|
+
developer.developRun(first.x, first.y, second.x - first.x, second.y - first.y, width, row);
|
|
317
|
+
rgb.set(row, y * width * 3);
|
|
318
|
+
await afterRow(y + 1, height);
|
|
319
|
+
}
|
|
320
|
+
const c = developer.counts;
|
|
321
|
+
return { rgb, highlight: { mode: v.highlightMode, saturatedPixelCount: c.saturatedPixelCount, whiteBalanceClippedSampleCount: c.whiteBalanceClippedSampleCount,
|
|
322
|
+
reconstructedPixelCount: c.reconstructedPixelCount } };
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* AHD development (2.1): the same site normalization as the native development kernel (flagged or white-level sites
|
|
326
|
+
* are 1, others (code − black)/(white − black)) on the whole native raster, demosaiced by `createAhdDemosaicTask`
|
|
327
|
+
* (mirrored plane edges), then the native development's own colour operator (`developInto`: WB, highlight mode,
|
|
328
|
+
* camera → XYZ → linear sRGB) per output pixel. The AHD homogeneity metric sees white-balanced colour: its
|
|
329
|
+
* camera → XYZ matrix is the profile's matrix times the WB gains (the output values are not transformed by it).
|
|
330
|
+
* The mosaic is stored in float32, as the AHD task requires.
|
|
331
|
+
*/
|
|
332
|
+
async function developAhd(v, raw, repeatWidth, repeatHeight, width, height, at, afterRow, host) {
|
|
333
|
+
const native = v.plan.exposure.geometry.nativeRaster, W = native.pixelWidth, H = native.pixelHeight;
|
|
334
|
+
const source = { nativeWidth: W, repeatWidth, repeatHeight, codes: raw.codes, blackLevels: raw.blackLevels,
|
|
335
|
+
saturationFlags: raw.saturationFlags, whiteByPhase: raw.effectiveSaturationCodes };
|
|
336
|
+
const samples = new Float32Array(W * H), saturated = new Uint8Array(W * H);
|
|
337
|
+
for (let y = 0; y < H; y++)
|
|
338
|
+
for (let x = 0; x < W; x++) {
|
|
339
|
+
samples[y * W + x] = normalizedRawSite(source, x, y);
|
|
340
|
+
if (rawSiteSaturated(source, x, y))
|
|
341
|
+
saturated[y * W + x] = 1;
|
|
342
|
+
}
|
|
343
|
+
const intent = v.plan.exposure.whiteBalanceIntent, gains = intent ? [intent.channelGains.red, intent.channelGains.green, intent.channelGains.blue] : [1, 1, 1];
|
|
344
|
+
const m = v.color.normalizedCameraChannelsToXyz;
|
|
345
|
+
const cameraToXyz = [0, 1, 2].map((r) => [0, 1, 2].map((c) => m[r][c] * gains[c]));
|
|
346
|
+
const task = createAhdDemosaicTask({ plane: { width: W, height: H, captureModeId: v.plan.frame.modeId, colorSamplingProfile: v.plan.frame.colorSamplingProfile, samples },
|
|
347
|
+
channelRoles: { red: "red", green: "green", blue: "blue" }, cameraToXyz }, { yieldControl: async () => { await host.yieldControl(); host.checkAbort(); } });
|
|
348
|
+
const demosaicStarted = clockMs();
|
|
349
|
+
await task.run();
|
|
350
|
+
host.onDemosaicMs?.(clockMs() - demosaicStarted);
|
|
351
|
+
const demosaiced = task.takeOutput().data, rgb = new Float64Array(width * height * 3);
|
|
352
|
+
const counts = { saturatedPixelCount: 0, whiteBalanceClippedSampleCount: 0, reconstructedPixelCount: 0 };
|
|
353
|
+
for (let y = 0; y < height; y++) {
|
|
354
|
+
for (let x = 0; x < width; x++) {
|
|
355
|
+
const p = at(x, y), i = p.y * W + p.x;
|
|
356
|
+
let clipped = false;
|
|
357
|
+
for (let dy = -AHD_VALUE_REACH; dy <= AHD_VALUE_REACH && !clipped; dy++)
|
|
358
|
+
for (let dx = -AHD_VALUE_REACH; dx <= AHD_VALUE_REACH; dx++) {
|
|
359
|
+
const sx = p.x + dx, sy = p.y + dy;
|
|
360
|
+
if (sx >= 0 && sy >= 0 && sx < W && sy < H && saturated[sy * W + sx] !== 0) {
|
|
361
|
+
clipped = true;
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (clipped)
|
|
366
|
+
counts.saturatedPixelCount++;
|
|
367
|
+
v.development.developInto(demosaiced[3 * i], demosaiced[3 * i + 1], demosaiced[3 * i + 2], rgb, (y * width + x) * 3, counts);
|
|
368
|
+
}
|
|
369
|
+
await afterRow(y + 1, height);
|
|
370
|
+
}
|
|
371
|
+
return { rgb, highlight: { mode: v.highlightMode, saturatedPixelCount: counts.saturatedPixelCount, whiteBalanceClippedSampleCount: counts.whiteBalanceClippedSampleCount,
|
|
372
|
+
reconstructedPixelCount: counts.reconstructedPixelCount } };
|
|
373
|
+
}
|
|
374
|
+
/** Linear sRGB/D65 → CIE XYZ (inverse of the development's XYZ → sRGB matrix) → linear target RGB/D65. */
|
|
375
|
+
function linearSrgbToTarget(target) {
|
|
376
|
+
return multiplyMatrices(resolveRgbColorSpace(target).value.xyzToRgb, resolveCaptureColorModel().cameraRgbToXyz);
|
|
377
|
+
}
|
|
378
|
+
/** Knee of the `perceptual` gamut mapping: chroma below 90 % of the gamut-boundary chroma (at constant L*, h) is untouched. */
|
|
379
|
+
const PERCEPTUAL_GAMUT_KNEE_FRACTION = 0.9;
|
|
380
|
+
/**
|
|
381
|
+
* SDR encoding from linear sRGB/D65:
|
|
382
|
+
* 1. Convert to the target primaries with a matrix.
|
|
383
|
+
* 2. Either clip each component to [0, 1], or, with `mapping`, compress chroma while keeping CIELAB hue
|
|
384
|
+
* and lightness (`prepareRgbGamutMapping`).
|
|
385
|
+
* 3. Apply the target transfer function and round to the nearest code (ties round up).
|
|
386
|
+
*/
|
|
387
|
+
function encodeSdrFromLinear(rgb, encoding, bitDepth, mapping) {
|
|
388
|
+
const m = linearSrgbToTarget(encoding), toXyz = resolveCaptureColorModel().cameraRgbToXyz, maximum = 2 ** bitDepth - 1;
|
|
389
|
+
const transfer = encoding === "rec2020" ? evaluateBt709Oetf : evaluateSrgbEncoding;
|
|
390
|
+
const samples = bitDepth === 8 ? new Uint8Array(rgb.length) : new Uint16Array(rgb.length);
|
|
391
|
+
let low = 0, high = 0, adjusted = 0, lightnessClipped = 0;
|
|
392
|
+
for (let i = 0; i < rgb.length; i += 3) {
|
|
393
|
+
const r = rgb[i], g = rgb[i + 1], b = rgb[i + 2];
|
|
394
|
+
if (mapping) {
|
|
395
|
+
const xyz = [0, 1, 2].map((k) => toXyz[k][0] * r + toXyz[k][1] * g + toXyz[k][2] * b);
|
|
396
|
+
const mapped = mapping.map(xyz);
|
|
397
|
+
if (mapped.adjusted)
|
|
398
|
+
adjusted++;
|
|
399
|
+
if (mapped.lightnessClipped)
|
|
400
|
+
lightnessClipped++;
|
|
401
|
+
for (let k = 0; k < 3; k++)
|
|
402
|
+
samples[i + k] = Math.floor(transfer(mapped.linearRgb[k]) * maximum + 0.5);
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
for (let k = 0; k < 3; k++) {
|
|
406
|
+
const row = m[k];
|
|
407
|
+
let value = row[0] * r + row[1] * g + row[2] * b;
|
|
408
|
+
if (value < 0) {
|
|
409
|
+
value = 0;
|
|
410
|
+
low++;
|
|
411
|
+
}
|
|
412
|
+
else if (value > 1) {
|
|
413
|
+
value = 1;
|
|
414
|
+
high++;
|
|
415
|
+
}
|
|
416
|
+
samples[i + k] = Math.floor(transfer(value) * maximum + 0.5);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
return { samples, adjustedPixelCount: adjusted, lightnessClippedPixelCount: lightnessClipped, diagnostics: { renderingNegativeSampleCount: low,
|
|
420
|
+
renderingAboveReferenceSampleCount: high, toneChangedSampleCount: 0, gamutClippedLowSampleCount: low, gamutClippedHighSampleCount: high, captureSaturation: "consumed-in-raw-development" } };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Simulates one still capture of a planar scene and returns the developed image, the packed RAW, the full
|
|
424
|
+
* underlying request/plan, provenance and every assumption made. See docs/QUICK_START.md.
|
|
425
|
+
*/
|
|
426
|
+
export async function simulateCapture(options) {
|
|
427
|
+
const started = Date.now();
|
|
428
|
+
requireInputObject(options, "simulateCapture options");
|
|
429
|
+
const top = allowKeys(options, "simulateCapture options", ["camera", "lens", "exposure", "scene", "subjectDistanceM", "illuminant",
|
|
430
|
+
"illuminanceLux", "output", "sensor", "motion", "signal", "onProgress"]);
|
|
431
|
+
const signal = top.signal, onProgress = top.onProgress;
|
|
432
|
+
if (signal !== undefined && (typeof signal !== "object" || signal === null || typeof signal.aborted !== "boolean"))
|
|
433
|
+
fail("signal must be an AbortSignal.");
|
|
434
|
+
if (onProgress !== undefined && typeof onProgress !== "function")
|
|
435
|
+
fail("onProgress must be a function.");
|
|
436
|
+
const assumptions = [];
|
|
437
|
+
const assume = (id, kind, value, explanation) => { assumptions.push({ id, kind, value, explanation }); };
|
|
438
|
+
function pick(record, key, path, fallback, explanation) {
|
|
439
|
+
if (record[key] !== undefined)
|
|
440
|
+
return record[key];
|
|
441
|
+
assume(path, "default", fallback, explanation);
|
|
442
|
+
return fallback;
|
|
443
|
+
}
|
|
444
|
+
const checkAbort = () => { if (signal?.aborted)
|
|
445
|
+
fail("simulateCapture aborted."); };
|
|
446
|
+
checkAbort();
|
|
447
|
+
// Camera
|
|
448
|
+
const cameraOption = pick(top, "camera", "camera", DEFAULTS.camera, "Generic Photivra tier preset 2.0.0 (synthetic educational values, not a real camera).");
|
|
449
|
+
let preset;
|
|
450
|
+
if (typeof cameraOption === "string")
|
|
451
|
+
preset = GENERIC_TIER_ASSETS.find((p) => p.tier === cameraOption) ?? fail("Unknown camera tier.");
|
|
452
|
+
else
|
|
453
|
+
preset = parseCallerCameraPreset(cameraOption);
|
|
454
|
+
const body = preset.body, lens = preset.lens;
|
|
455
|
+
// Lens
|
|
456
|
+
const lensOptions = allowKeys(top.lens, "lens", ["focalLengthMm", "fNumber", "focusDistanceM", "pupilSampling", "projection", "distortion", "vignetting", "psf", "diffraction"]);
|
|
457
|
+
const focalDefault = lens.exposure.focalLengthMmRange.value.minimum;
|
|
458
|
+
const focalLengthMm = positive(pick(lensOptions, "focalLengthMm", "lens.focalLengthMm", focalDefault, "The preset lens's focal length (the generic primes are 50 mm)."), "lens.focalLengthMm");
|
|
459
|
+
const capabilities = resolveGenericEquipmentExposureCapabilities({ bodyProfile: body.exposure, lensProfile: lens.exposure, selectedFocalLengthMm: focalLengthMm });
|
|
460
|
+
const subjectDistanceM = positive(pick(top, "subjectDistanceM", "subjectDistanceM", DEFAULTS.subjectDistanceM, "Scene plane 5 m in front of the camera (the generic tiers' reference focus distance)."), "subjectDistanceM");
|
|
461
|
+
if (subjectDistanceM * 1000 <= focalLengthMm * 2)
|
|
462
|
+
fail("subjectDistanceM must exceed twice the focal length.");
|
|
463
|
+
const focusDistanceM = positive(pick(lensOptions, "focusDistanceM", "lens.focusDistanceM", subjectDistanceM, "Focused on the scene plane."), "lens.focusDistanceM");
|
|
464
|
+
if (focusDistanceM * 1000 <= focalLengthMm)
|
|
465
|
+
fail("lens.focusDistanceM must exceed the focal length.");
|
|
466
|
+
let pupil;
|
|
467
|
+
if (lensOptions.pupilSampling === undefined)
|
|
468
|
+
assume("lens.pupilSampling", "default", null, "No pupil sampling: each scene query follows the central (chief) ray, so defocus and depth of field are not rendered. Enable lens.pupilSampling for an ideal circular pupil.");
|
|
469
|
+
else {
|
|
470
|
+
const p = allowKeys(lensOptions.pupilSampling, "lens.pupilSampling", ["radialSamples", "angularSamples"]);
|
|
471
|
+
// The environment pupil contract admits 4..64 azimuthal midpoints and at most 256 rays.
|
|
472
|
+
pupil = { radialSamples: integerIn(p.radialSamples, "lens.pupilSampling.radialSamples", 1, 16), angularSamples: integerIn(p.angularSamples, "lens.pupilSampling.angularSamples", 4, 64) };
|
|
473
|
+
if (pupil.radialSamples * pupil.angularSamples > 256)
|
|
474
|
+
fail("lens.pupilSampling needs at most 256 rays (radialSamples × angularSamples).");
|
|
475
|
+
}
|
|
476
|
+
// 2.0 optics hooks: only what the environment site contract accepts.
|
|
477
|
+
const projection = lensOptions.projection === undefined ? undefined : parseLensProjectionModel(lensOptions.projection);
|
|
478
|
+
let distortion;
|
|
479
|
+
if (lensOptions.distortion !== undefined) {
|
|
480
|
+
const d = allowKeys(lensOptions.distortion, "lens.distortion", ["kind", "profile"]);
|
|
481
|
+
if (d.kind !== "radial" && d.kind !== "brown-conrady")
|
|
482
|
+
fail("lens.distortion.kind must be radial or brown-conrady.");
|
|
483
|
+
distortion = d;
|
|
484
|
+
}
|
|
485
|
+
const lensFieldMapping = projection === undefined && distortion === undefined ? undefined :
|
|
486
|
+
{ projectionModel: projection ?? "rectilinear", distortion: distortion ?? { kind: "none" }, radiometricAreaCorrection: "inverse-distortion-jacobian" };
|
|
487
|
+
if (lensFieldMapping) {
|
|
488
|
+
if (pupil)
|
|
489
|
+
fail("lens.projection and lens.distortion cannot be combined with lens.pupilSampling: ideal pupil rays use the rectilinear, distortion-free projection.");
|
|
490
|
+
try {
|
|
491
|
+
prepareEnvironmentLensFieldMapping(lensFieldMapping);
|
|
492
|
+
}
|
|
493
|
+
catch (error) {
|
|
494
|
+
fail("Invalid lens projection or distortion: " + error.message);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
let vignetting;
|
|
498
|
+
if (lensOptions.vignetting !== undefined) {
|
|
499
|
+
requireInputObject(lensOptions.vignetting, "lens.vignetting");
|
|
500
|
+
vignetting = lensOptions.vignetting;
|
|
501
|
+
}
|
|
502
|
+
let lensPsf;
|
|
503
|
+
if (lensOptions.psf !== undefined) {
|
|
504
|
+
const p = allowKeys(lensOptions.psf, "lens.psf", ["kind", "configuration"]);
|
|
505
|
+
if (p.kind !== "sampled-local")
|
|
506
|
+
fail("lens.psf.kind must be sampled-local.");
|
|
507
|
+
requireInputObject(p.configuration, "lens.psf.configuration");
|
|
508
|
+
if (pupil)
|
|
509
|
+
fail("lens.psf cannot be combined with lens.pupilSampling: geometric defocus would be counted twice.");
|
|
510
|
+
lensPsf = p;
|
|
511
|
+
}
|
|
512
|
+
// Diffraction (2.1): validated here, built once the f-number is resolved.
|
|
513
|
+
let diffractionRadiusOption;
|
|
514
|
+
if (lensOptions.diffraction !== undefined) {
|
|
515
|
+
const d = lensOptions.diffraction;
|
|
516
|
+
if (d === "circular-aperture")
|
|
517
|
+
diffractionRadiusOption = null;
|
|
518
|
+
else {
|
|
519
|
+
const record = allowKeys(d, "lens.diffraction", ["aperture", "kernelRadiusSamples"]);
|
|
520
|
+
if (record.aperture !== "circular-aperture")
|
|
521
|
+
fail("lens.diffraction must be \"circular-aperture\" or { aperture: \"circular-aperture\", kernelRadiusSamples }.");
|
|
522
|
+
diffractionRadiusOption = record.kernelRadiusSamples === undefined ? null : integerIn(record.kernelRadiusSamples, "lens.diffraction.kernelRadiusSamples", 0, 32);
|
|
523
|
+
}
|
|
524
|
+
if (lensPsf)
|
|
525
|
+
fail("lens.diffraction cannot be combined with lens.psf: diffraction would be counted twice.");
|
|
526
|
+
if (pupil)
|
|
527
|
+
fail("lens.diffraction cannot be combined with lens.pupilSampling: the environment contract does not mix a sampled PSF with pupil rays.");
|
|
528
|
+
}
|
|
529
|
+
// Motion (2.0): constant camera rotation and the shutter type.
|
|
530
|
+
const motionOptions = allowKeys(top.motion, "motion", ["angularVelocityRadPerSec", "shutter", "rollingShutterSkewSeconds"]);
|
|
531
|
+
const angular = { pitch: 0, yaw: 0, roll: 0 };
|
|
532
|
+
if (motionOptions.angularVelocityRadPerSec !== undefined) {
|
|
533
|
+
const a = allowKeys(motionOptions.angularVelocityRadPerSec, "motion.angularVelocityRadPerSec", ["pitch", "yaw", "roll"]);
|
|
534
|
+
for (const axis of ["pitch", "yaw", "roll"])
|
|
535
|
+
if (a[axis] !== undefined) {
|
|
536
|
+
const value = a[axis];
|
|
537
|
+
if (typeof value !== "number" || !Number.isFinite(value) || Math.abs(value) > 100)
|
|
538
|
+
fail("motion.angularVelocityRadPerSec." + axis + " must be a finite number within ±100 rad/s.");
|
|
539
|
+
angular[axis] = value;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const rotating = angular.pitch !== 0 || angular.yaw !== 0 || angular.roll !== 0;
|
|
543
|
+
const shutterKind = motionOptions.shutter === undefined ? "global" : motionOptions.shutter;
|
|
544
|
+
if (shutterKind !== "global" && shutterKind !== "rolling")
|
|
545
|
+
fail("motion.shutter must be global or rolling.");
|
|
546
|
+
if (motionOptions.rollingShutterSkewSeconds !== undefined && shutterKind !== "rolling")
|
|
547
|
+
fail("motion.rollingShutterSkewSeconds needs motion.shutter \"rolling\".");
|
|
548
|
+
const rollingSkewOption = motionOptions.rollingShutterSkewSeconds === undefined ? undefined : positive(motionOptions.rollingShutterSkewSeconds, "motion.rollingShutterSkewSeconds");
|
|
549
|
+
// Diffraction runs as an object-space convolution when every PSF tap's source is another site centre seen
|
|
550
|
+
// through one linear map (still camera, rectilinear, no distortion or field throughput); otherwise per tap.
|
|
551
|
+
const diffractionObjectSpace = diffractionRadiusOption !== undefined && !rotating && !lensFieldMapping && !vignetting;
|
|
552
|
+
// Output and sensor
|
|
553
|
+
const outputOptions = allowKeys(top.output, "output", ["width", "height", "format", "toneCurve", "toneCurveParameters", "highlightMode", "whiteBalance", "jpegQuantizationStep",
|
|
554
|
+
"colorEncoding", "bitDepth", "gamutMapping", "demosaic", "borderHandling", "dng"]);
|
|
555
|
+
const width = integerIn(pick(outputOptions, "width", "output.width", DEFAULTS.width, "64 × 64 output pixels."), "output.width", 1, 4094);
|
|
556
|
+
const height = integerIn(pick(outputOptions, "height", "output.height", DEFAULTS.height, "64 × 64 output pixels."), "output.height", 1, 4094);
|
|
557
|
+
const format = pick(outputOptions, "format", "output.format", DEFAULTS.format, "8-bit sRGB samples.");
|
|
558
|
+
if (!["srgb-8", "srgb-16", "jpeg", "png", "dng"].includes(format))
|
|
559
|
+
fail("output.format must be srgb-8, srgb-16, jpeg, png or dng.");
|
|
560
|
+
let dngCapturedAtUtc;
|
|
561
|
+
if (format === "dng") {
|
|
562
|
+
const d = allowKeys(outputOptions.dng, "output.dng", ["capturedAtUtc"]);
|
|
563
|
+
dngCapturedAtUtc = d.capturedAtUtc;
|
|
564
|
+
if (dngCapturedAtUtc === undefined)
|
|
565
|
+
assume("output.dng.capturedAtUtc", "default", null, "No capture time: the DNG carries no DateTimeOriginal (the engine never invents a wall clock).");
|
|
566
|
+
}
|
|
567
|
+
else if (outputOptions.dng !== undefined)
|
|
568
|
+
fail("output.dng applies only to output.format \"dng\".");
|
|
569
|
+
let colorEncoding, pngBitDepth;
|
|
570
|
+
if (format === "png") {
|
|
571
|
+
colorEncoding = pick(outputOptions, "colorEncoding", "output.colorEncoding", "srgb", "sRGB (W3C CSS Color 4 srgb) samples signalled with the PNG sRGB chunk (perceptual intent) and its prescribed gAMA/cHRM values.");
|
|
572
|
+
if (!["srgb", "display-p3", "rec2020"].includes(colorEncoding))
|
|
573
|
+
fail("output.colorEncoding must be srgb, display-p3 or rec2020.");
|
|
574
|
+
pngBitDepth = pick(outputOptions, "bitDepth", "output.bitDepth", 8, "8 bits per sample.");
|
|
575
|
+
if (pngBitDepth !== 8 && pngBitDepth !== 16)
|
|
576
|
+
fail("output.bitDepth must be 8 or 16.");
|
|
577
|
+
}
|
|
578
|
+
else if (outputOptions.colorEncoding !== undefined || outputOptions.bitDepth !== undefined)
|
|
579
|
+
fail("output.colorEncoding and output.bitDepth apply only to output.format \"png\".");
|
|
580
|
+
let perceptual = false;
|
|
581
|
+
if (format === "png") {
|
|
582
|
+
const gamutMapping = pick(outputOptions, "gamutMapping", "output.gamutMapping", "clip", "Out-of-gamut components are clipped to [0, 1] in the target space.");
|
|
583
|
+
if (gamutMapping !== "clip" && gamutMapping !== "perceptual")
|
|
584
|
+
fail("output.gamutMapping must be clip or perceptual.");
|
|
585
|
+
perceptual = gamutMapping === "perceptual";
|
|
586
|
+
}
|
|
587
|
+
else if (outputOptions.gamutMapping !== undefined)
|
|
588
|
+
fail("output.gamutMapping applies only to output.format \"png\".");
|
|
589
|
+
const wideGamut = colorEncoding !== undefined && colorEncoding !== "srgb";
|
|
590
|
+
// The linear development path serves wide-gamut encodings and perceptual gamut mapping.
|
|
591
|
+
const linearPath = wideGamut || perceptual;
|
|
592
|
+
const toneCurve = pick(outputOptions, "toneCurve", "output.toneCurve", DEFAULTS.toneCurve, "Identity tone curve: scene-linear values are clipped at display white, then sRGB-encoded.");
|
|
593
|
+
if (linearPath && toneCurve !== "identity")
|
|
594
|
+
fail("output.toneCurve other than identity is available only for sRGB output; wide-gamut encodings are converted from the linear developed colour.");
|
|
595
|
+
const highlightMode = pick(outputOptions, "highlightMode", "output.highlightMode", DEFAULTS.highlightMode, "Saturated RAW highlights are clipped to neutral.");
|
|
596
|
+
const whiteBalanceOption = pick(outputOptions, "whiteBalance", "output.whiteBalance", DEFAULTS.whiteBalance, "White balance measured from a perfect white diffuser under the scene illuminant (a custom/grey-card white balance).");
|
|
597
|
+
const jpegStep = format === "jpeg" ? integerIn(pick(outputOptions, "jpegQuantizationStep", "output.jpegQuantizationStep", DEFAULTS.jpegQuantizationStep, "Constant JPEG quantization step 2 (not a manufacturer quality scale)."), "output.jpegQuantizationStep", 1, 255) : undefined;
|
|
598
|
+
if (format !== "jpeg" && outputOptions.jpegQuantizationStep !== undefined)
|
|
599
|
+
fail("output.jpegQuantizationStep applies only to output.format \"jpeg\".");
|
|
600
|
+
const sensorOptions = allowKeys(top.sensor, "sensor", ["pixelPitchMicrometers", "spectralStepNanometers", "temporalSampleCount", "noiseSeed", "antiAliasing"]);
|
|
601
|
+
const pitch = positive(pick(sensorOptions, "pixelPitchMicrometers", "sensor.pixelPitchMicrometers", DEFAULTS.pixelPitchMicrometers, "6 µm pixels (about a 24 MP full-frame sensor). The simulated sensor is width × pitch wide, so small images are a central crop of a large sensor, not a downscaled full frame."), "sensor.pixelPitchMicrometers");
|
|
602
|
+
if (pitch < 0.5 || pitch > 100)
|
|
603
|
+
fail("sensor.pixelPitchMicrometers must be 0.5-100.");
|
|
604
|
+
const spectralStep = positive(pick(sensorOptions, "spectralStepNanometers", "sensor.spectralStepNanometers", DEFAULTS.spectralStepNanometers, "Spectral midpoint quadrature over 380-780 nm with subintervals of at most 20 nm."), "sensor.spectralStepNanometers");
|
|
605
|
+
if (spectralStep < 1 || spectralStep > 100)
|
|
606
|
+
fail("sensor.spectralStepNanometers must be 1-100.");
|
|
607
|
+
const temporalSampleCount = integerIn(rotating ? pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", ROTATING_TEMPORAL_SAMPLE_COUNT, "Eight temporal midpoints across each site's exposure window sample the camera rotation (motion blur); raise it for long exposures or fast rotation.") :
|
|
608
|
+
pick(sensorOptions, "temporalSampleCount", "sensor.temporalSampleCount", DEFAULTS.temporalSampleCount, "One temporal sample: the static scene and still camera make more samples unnecessary."), "sensor.temporalSampleCount", 1, 256);
|
|
609
|
+
const noiseSeed = integerIn(pick(sensorOptions, "noiseSeed", "sensor.noiseSeed", DEFAULTS.noiseSeed, "Fixed noise seed 1 (deterministic output)."), "sensor.noiseSeed", 0, 0xffffffff);
|
|
610
|
+
const antiAliasing = sensorOptions.antiAliasing === undefined ? undefined : parseAntiAliasing(sensorOptions.antiAliasing, pitch);
|
|
611
|
+
const colorSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.cfa, evidence: FACADE_EVIDENCE, coordinateSystem: "native-sensor-color-sampling-site-index",
|
|
612
|
+
layout: { kind: "periodic-mosaic", repeatWidthSites: 2, repeatHeightSites: 2, siteChannelIds: ["red", "green", "green", "blue"], anchor: "native-sensor-top-left-site" } };
|
|
613
|
+
// Demosaic (2.0): built-in Bayer kernels and border handling. Omitted, the 1.0 bilinear kernels and a
|
|
614
|
+
// one-site rendered border are used unchanged.
|
|
615
|
+
const demosaicOption = outputOptions.demosaic, borderOption = outputOptions.borderHandling;
|
|
616
|
+
if (demosaicOption !== undefined && demosaicOption !== "bilinear" && demosaicOption !== "ahd")
|
|
617
|
+
fail("output.demosaic must be bilinear or ahd.");
|
|
618
|
+
const ahd = demosaicOption === "ahd";
|
|
619
|
+
if (borderOption !== undefined && borderOption !== "margin" && borderOption !== "mirror")
|
|
620
|
+
fail("output.borderHandling must be margin or mirror.");
|
|
621
|
+
const builtInDemosaic = demosaicOption === undefined ? undefined : createBuiltInBayerDemosaicProfiles({ kernel: "bilinear",
|
|
622
|
+
captureModeId: IDS.mode, colorSamplingProfile, channelRoles: { red: "red", green: "green", blue: "blue" } }).value;
|
|
623
|
+
const mirrorBorder = borderOption === "mirror";
|
|
624
|
+
const margin = mirrorBorder ? 0 : ahd ? AHD_MARGIN : builtInDemosaic ? builtInDemosaic.kernelReachSites : MARGIN;
|
|
625
|
+
if (mirrorBorder && (width < 2 || height < 2))
|
|
626
|
+
fail("output.borderHandling \"mirror\" needs an output of at least 2 × 2 pixels.");
|
|
627
|
+
if (ahd && mirrorBorder && (width < 3 || height < 3))
|
|
628
|
+
fail("output.demosaic \"ahd\" with mirror borders needs an output of at least 3 × 3 pixels.");
|
|
629
|
+
// Geometry: the demosaic border (if any) on each side; output crop is the active area.
|
|
630
|
+
const nativeWidth = width + 2 * margin, nativeHeight = height + 2 * margin;
|
|
631
|
+
const imagingArea = { widthMm: nativeWidth * pitch / 1000, heightMm: nativeHeight * pitch / 1000 };
|
|
632
|
+
const geometry = { imagingArea, nativeRaster: { pixelWidth: nativeWidth, pixelHeight: nativeHeight }, orientation: "landscape",
|
|
633
|
+
activeCaptureRect: { x: margin, y: margin, width, height }, outputRaster: { pixelWidth: width, pixelHeight: height } };
|
|
634
|
+
const resolvedGeometry = resolveCaptureGeometry(geometry).value;
|
|
635
|
+
const imageDistanceMm = focalLengthMm * focusDistanceM * 1000 / (focusDistanceM * 1000 - focalLengthMm);
|
|
636
|
+
const metresPerFrameUnit = (width * pitch / 2000) * subjectDistanceM / imageDistanceMm;
|
|
637
|
+
const aspect = height / width;
|
|
638
|
+
assume("scene.framing", "derived", { framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect }, "The scene plane is perpendicular to the optical axis and the chart is sized to fill the active frame exactly.");
|
|
639
|
+
if (demosaicOption === undefined && borderOption === undefined) {
|
|
640
|
+
assume("output.demosaic", "model", "bilinear-rggb", "Bilinear RGGB demosaic through explicit linear kernels; the native raster has a one-site border on each side so every output pixel has full support.");
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
if (demosaicOption === undefined)
|
|
644
|
+
assume("output.demosaic", "default", "bilinear-rggb", "The 1.0 facade's bilinear RGGB kernels (explicit linear neighbourhood weights).");
|
|
645
|
+
else if (ahd)
|
|
646
|
+
assume("output.demosaic", "model", { method: "ahd-hirakawa-parks-2005", dependenceReachSites: AHD_MARGIN }, "Adaptive homogeneity-directed demosaicing (createAhdDemosaicTask, after Hirakawa & Parks, IEEE TIP 14(3), 2005): directional green, red/blue from colour differences, and per pixel the more homogeneous of the horizontal and vertical candidates in CIELAB (white-balanced camera colour); the paper's iterative median step is not applied. Non-linear; the mosaic is normalized and white-level clipped as in the native development and stored in float32, and the native development's colour operator follows. request.development keeps the built-in bilinear kernels, which validate the development but are not used for these pixels.");
|
|
647
|
+
else
|
|
648
|
+
assume("output.demosaic", "model", { kernel: builtInDemosaic.kernel, modelVersion: BUILT_IN_DEMOSAIC_VERSION, kernelReachSites: builtInDemosaic.kernelReachSites }, "Built-in bilinear Bayer kernels (createBuiltInBayerDemosaicProfiles): nearest same-channel averages.");
|
|
649
|
+
if (borderOption === undefined)
|
|
650
|
+
assume("output.borderHandling", "default", "margin", "The native raster adds the kernel reach (" + margin + " site(s)) on each side, so every output pixel is reconstructed from rendered sites.");
|
|
651
|
+
else
|
|
652
|
+
assume("output.borderHandling", "model", borderOption, mirrorBorder ?
|
|
653
|
+
"No rendered border: the native raster equals the output and reconstruction reads reflect-101 mirror images of the missing sites (CFA consistency validated), so edge pixels use mirrored rather than rendered neighbours." :
|
|
654
|
+
"The native raster adds the kernel reach (" + margin + " site(s)) on each side, so every output pixel is reconstructed from rendered sites.");
|
|
655
|
+
}
|
|
656
|
+
// Radiance queries per native site: spectral subintervals × temporal midpoints × pupil rays × PSF taps.
|
|
657
|
+
const psfTaps = lensPsf ? Math.max(1, ...(lensPsf.configuration.psf.profile.nodes ?? [])
|
|
658
|
+
.map((node) => Number(node.kernel?.widthSamples) * Number(node.kernel?.heightSamples)).filter((taps) => Number.isSafeInteger(taps) && taps > 0)) : 1;
|
|
659
|
+
const queriesPerSite = temporalSampleCount * (Math.ceil((FACADE_SPECTRAL_RANGE_NANOMETERS.maximum - FACADE_SPECTRAL_RANGE_NANOMETERS.minimum) / spectralStep) + 1) *
|
|
660
|
+
(pupil ? pupil.radialSamples * pupil.angularSamples : 1) * psfTaps * (antiAliasing ? antiAliasing.components.length : 1);
|
|
661
|
+
// Scene and illuminant
|
|
662
|
+
// The scene is the one required input: the facade never guesses what is being photographed.
|
|
663
|
+
if (top.scene === undefined)
|
|
664
|
+
fail("simulateCapture needs a scene, for example { preset: \"uniform\" } (an 18 % grey card) or { preset: \"slanted-edge\" }.");
|
|
665
|
+
const sceneOption = top.scene;
|
|
666
|
+
requireInputObject(sceneOption, "scene");
|
|
667
|
+
const illuminantOption = pick(top, "illuminant", "illuminant", { kind: "planckian", temperatureKelvin: DEFAULTS.illuminantKelvin }, "Planckian (blackbody) 6500 K as a stand-in for daylight. CIE D65 is not bundled; pass its S0/S1/S2 basis as a daylight illuminant to use it.");
|
|
668
|
+
const illuminant = resolveCaptureIlluminant(illuminantOption);
|
|
669
|
+
const illuminanceLux = positive(pick(top, "illuminanceLux", "illuminanceLux", SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX, "π·10·N²/(0.65·t·S·0.18) lux at f/8, 1/125 s, ISO 100 (CIPA DC-004 standard output sensitivity H = 10/S and camera equation H = 0.65·L·t/N²): an 18 % grey card then gets the reference exposure for the default settings. It does not follow the caller's exposure settings."), "illuminanceLux");
|
|
670
|
+
const irradianceScale = calculateIlluminantIrradianceScale(illuminant.shape, illuminanceLux);
|
|
671
|
+
let chart, callerScene;
|
|
672
|
+
const sceneRecord = sceneOption;
|
|
673
|
+
if ("kind" in sceneRecord) {
|
|
674
|
+
callerScene = requireCallerScene(sceneOption);
|
|
675
|
+
}
|
|
676
|
+
else {
|
|
677
|
+
chart = resolveChartScene({ scene: sceneOption, aspect });
|
|
678
|
+
for (const key of chart.defaultedParameters)
|
|
679
|
+
assume("scene." + key, "default", chart.parameters[key], "Chart default for " + chart.preset + ".");
|
|
680
|
+
}
|
|
681
|
+
const scenePreset = chart ? chart.preset : callerScene.kind;
|
|
682
|
+
// Caller spectra enter the digest as data (see spectrumDigestForm). Without caller spectra the description is unchanged,
|
|
683
|
+
// so default captures keep their digests.
|
|
684
|
+
const illuminantRecord = typeof illuminantOption === "object" ? illuminantOption : undefined;
|
|
685
|
+
const illuminantSpectrum = illuminantRecord?.kind === "spectrum" ? { spectrum: spectrumDigestForm(illuminant.spectrum, illuminant.shape) } :
|
|
686
|
+
illuminantRecord?.kind === "daylight" ? { basisDatasetId: illuminantRecord.basis?.datasetId, spectrum: spectrumDigestForm(illuminant.spectrum, illuminant.shape) } : undefined;
|
|
687
|
+
const sceneSpectra = chart ? chart.materials.flatMap((m, index) => {
|
|
688
|
+
if (m.kind !== "spectrum")
|
|
689
|
+
return [];
|
|
690
|
+
const raw = chart.preset === "uniform" ? sceneRecord.reflectance : sceneRecord.patches[index].reflectance;
|
|
691
|
+
return [{ material: index, spectrum: spectrumDigestForm(raw, m.spectrum) }];
|
|
692
|
+
}) : [];
|
|
693
|
+
const sceneDescription = { facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, scene: chart ? { preset: chart.preset, parameters: chart.parameters } :
|
|
694
|
+
{ kind: callerScene.kind, sceneId: callerScene.sceneId }, illuminant: illuminant.id, illuminanceLux, subjectDistanceM, framedWidthM: 2 * metresPerFrameUnit, aspect,
|
|
695
|
+
...(illuminantSpectrum === undefined ? {} : { illuminantSpectrum }), ...(sceneSpectra.length === 0 ? {} : { sceneSpectra }) };
|
|
696
|
+
if (callerScene)
|
|
697
|
+
assume("scene.digest", "model", "caller-function-not-hashed", "The source digest covers the caller's sceneId and the framing, not the body of the caller's evaluate function.");
|
|
698
|
+
const sourceSha256 = await sha256Hex(JSON.stringify(sceneDescription, (_key, value) => (typeof value === "object" && value !== null && "evaluate" in value ? "caller-spectrum" : value)));
|
|
699
|
+
checkAbort();
|
|
700
|
+
const provider = buildPlanarSceneRadianceProvider({ scene: chart ?? callerScene, sceneId: IDS.scene, providerProfileId: IDS.provider,
|
|
701
|
+
illuminant, irradianceScale, geometry: { distanceM: subjectDistanceM, metresPerFrameUnit }, evidence: FACADE_EVIDENCE,
|
|
702
|
+
limitations: ["Planar Lambertian scene under uniform illumination; no shadows, inter-reflection, gloss or fluorescence."] });
|
|
703
|
+
assume("model.scene-radiometry", "model", "lambertian-plane", "Radiance = reflectance × irradiance / π on one plane; illuminance from K_m = 683 lm/W and the engine's analytic CIE 1931 ȳ fit.");
|
|
704
|
+
// Metering (CIPA DC-004 standard output sensitivity and camera equation) and exposure-mode resolution
|
|
705
|
+
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops"]);
|
|
706
|
+
const mode = pick(exposureOptions, "mode", "exposure.mode", "manual", "Manual exposure.");
|
|
707
|
+
if (!["manual", "aperture-priority", "shutter-priority", "program"].includes(mode))
|
|
708
|
+
fail("Unknown exposure mode.");
|
|
709
|
+
const iso = positive(pick(exposureOptions, "iso", "exposure.iso", DEFAULTS.iso, "ISO 100, the tiers' base ISO."), "exposure.iso");
|
|
710
|
+
const compensation = exposureOptions.compensationStops === undefined ? DEFAULTS.compensationStops : exposureOptions.compensationStops;
|
|
711
|
+
if (typeof compensation !== "number" || !Number.isFinite(compensation) || Math.abs(compensation) > 10)
|
|
712
|
+
fail("exposure.compensationStops must be within ±10.");
|
|
713
|
+
if (exposureOptions.compensationStops === undefined)
|
|
714
|
+
assume("exposure.compensationStops", "default", 0, "No exposure compensation.");
|
|
715
|
+
const reference = { aperture: DEFAULTS.fNumber, shutterSeconds: DEFAULTS.shutterSeconds, iso: DEFAULTS.iso };
|
|
716
|
+
const referenceLuminance = sosReferenceLuminance(reference.aperture, reference.shutterSeconds, reference.iso);
|
|
717
|
+
assume("exposure.referenceExposure", "model", { ...reference, meteredLuminanceCdPerSquareMeter: referenceLuminance }, "Exposure anchor: f/8, 1/125 s, ISO 100 gives the CIPA DC-004 standard output sensitivity reference exposure H = 10/S = 0.1 lx·s for an average scene luminance of 10·N²/(0.65·t·S) ≈ 1230.77 cd/m² (camera equation H = 0.65·L·t/N²).");
|
|
718
|
+
const meterProfile = body.metering[0].profile, zones = 16, zoneSamples = [];
|
|
719
|
+
let luminanceSum = 0;
|
|
720
|
+
for (let j = 0; j < zones; j++)
|
|
721
|
+
for (let i = 0; i < zones; i++) {
|
|
722
|
+
const u = -1 + (i + 0.5) * 2 / zones, v = aspect - (j + 0.5) * 2 * aspect / zones;
|
|
723
|
+
const luminance = luminanceOf((nm) => provider.radianceAt(u, v, nm));
|
|
724
|
+
luminanceSum += luminance;
|
|
725
|
+
zoneSamples.push({ sampleId: "zone-" + i + "-" + j, positionOrientedCaptureUv: { u: (i + 0.5) / zones, v: (j + 0.5) / zones },
|
|
726
|
+
relativeLinearSignal: meterProfile.target.targetRelativeSignal * luminance / referenceLuminance, areaWeight: 1 });
|
|
727
|
+
}
|
|
728
|
+
const meteredLuminance = luminanceSum / (zones * zones);
|
|
729
|
+
assume("exposure.metering", "model", { policy: meterProfile.policy.kind, zones: zones * zones }, "The body's " + meterProfile.policy.kind + " meter reads scene luminance on a 16 × 16 zone grid, before the lens (no lens transmission, vignetting or flare).");
|
|
730
|
+
const meterResult = meterRelativeExposure({ profile: meterProfile, sampleSet: { measurementId: "facade-meter", sceneStateId: IDS.sceneState,
|
|
731
|
+
inputDomain: "relative-pre-exposure-linear-signal", captureRegion: "oriented-active-capture", captureGeometry: resolvedGeometry,
|
|
732
|
+
processingState: { exposureSettingsApplied: false, whiteBalanceApplied: false, toneMappingApplied: false, displayGammaApplied: false, sharpeningApplied: false },
|
|
733
|
+
samples: zoneSamples } });
|
|
734
|
+
let target = createExposureMeterTargetFromMeteringResult({ targetId: "facade-target", meterResult: meterResult.value });
|
|
735
|
+
if (compensation !== 0)
|
|
736
|
+
target = setExposureCompensationOnMeterTarget({ targetId: "facade-target-ec", baseTarget: target, exposureCompensationStops: compensation });
|
|
737
|
+
let resolution;
|
|
738
|
+
let fNumber, shutterSeconds;
|
|
739
|
+
if (mode === "manual" || mode === "aperture-priority") {
|
|
740
|
+
fNumber = positive(pick(lensOptions, "fNumber", "lens.fNumber", DEFAULTS.fNumber, "f/8."), "lens.fNumber");
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
if (lensOptions.fNumber !== undefined)
|
|
744
|
+
fail("lens.fNumber is resolved automatically in " + mode + " mode; omit it.");
|
|
745
|
+
fNumber = NaN;
|
|
746
|
+
}
|
|
747
|
+
if (mode === "manual" || mode === "shutter-priority") {
|
|
748
|
+
shutterSeconds = positive(pick(exposureOptions, "shutterSeconds", "exposure.shutterSeconds", DEFAULTS.shutterSeconds, "1/125 s."), "exposure.shutterSeconds");
|
|
749
|
+
}
|
|
750
|
+
else {
|
|
751
|
+
if (exposureOptions.shutterSeconds !== undefined)
|
|
752
|
+
fail("exposure.shutterSeconds is resolved automatically in " + mode + " mode; omit it.");
|
|
753
|
+
shutterSeconds = NaN;
|
|
754
|
+
}
|
|
755
|
+
if (mode === "manual") {
|
|
756
|
+
resolution = resolveManualExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualShutterSeconds: shutterSeconds,
|
|
757
|
+
isoControl: { kind: "manual", iso } });
|
|
758
|
+
}
|
|
759
|
+
else if (mode === "aperture-priority") {
|
|
760
|
+
resolution = resolveAperturePriorityExposureMode({ target, capabilities, referenceExposure: reference, manualAperture: fNumber, manualIso: iso,
|
|
761
|
+
shutterQuantizationPolicy: "nearest-log2-shorter-on-tie" });
|
|
762
|
+
}
|
|
763
|
+
else if (mode === "shutter-priority") {
|
|
764
|
+
resolution = resolveShutterPriorityExposureMode({ target, capabilities, referenceExposure: reference, manualShutterSeconds: shutterSeconds,
|
|
765
|
+
isoControl: { kind: "manual", iso, apertureQuantizationPolicy: "nearest-log2-narrower-on-tie" } });
|
|
766
|
+
}
|
|
767
|
+
else {
|
|
768
|
+
assume("exposure.programLine", "model", "facade-generic-program-line", "Engine-authored generic program line (f/16 1/2000 s … f/8 1/125 s … f/4 1.024 s); not a manufacturer program.");
|
|
769
|
+
resolution = resolveProgramAutoExposureMode({ target, capabilities, referenceExposure: reference, programLine: defaultProgramLine(), isoControl: { kind: "manual", iso } });
|
|
770
|
+
}
|
|
771
|
+
if (resolution.status !== "resolved" || !resolution.resolvedSettings)
|
|
772
|
+
fail("Exposure could not be resolved (" + resolution.status + "); the scene may have no signal.");
|
|
773
|
+
fNumber = resolution.resolvedSettings.aperture ?? fNumber;
|
|
774
|
+
shutterSeconds = resolution.resolvedSettings.shutterSeconds ?? shutterSeconds;
|
|
775
|
+
const resolvedIso = resolution.resolvedSettings.iso ?? iso;
|
|
776
|
+
// Sensor readout for the ISO setting
|
|
777
|
+
const regimeByChannel = {};
|
|
778
|
+
for (const channel of CHANNELS) {
|
|
779
|
+
const asset = body.readout.find((a) => a.profile.channelId === channel) ?? fail("Camera readout lacks channel " + channel + ".");
|
|
780
|
+
const chain = resolveGenericIsoSignalChain({ profile: body.signalChain, isoCapability: body.iso, requestedIsoSetting: { kind: "standard", exposureIndex: resolvedIso },
|
|
781
|
+
captureModeId: IDS.mode, readoutProfile: asset.profile });
|
|
782
|
+
regimeByChannel[channel] = { readoutProfile: asset.profile, regimeId: chain.readout.regime.regimeId };
|
|
783
|
+
}
|
|
784
|
+
const baseRegime = body.readout[0].profile.regimes[0];
|
|
785
|
+
const capacityElectrons = Math.max(...body.readout[0].profile.regimes.map((r) => r.preAdcSaturationElectronEquivalent.value), baseRegime.preAdcSaturationElectronEquivalent.value);
|
|
786
|
+
assume("sensor.capacityElectrons", "derived", capacityElectrons, "Physical charge capacity set to the body's largest declared pre-ADC saturation (its base-ISO saturation signal), so storage never clips before the declared readout white.");
|
|
787
|
+
assume("sensor.darkCurrent", "model", { electronsPerSecond: DARK_CURRENT_ELECTRONS_PER_SECOND, temperatureC: OPERATING_TEMPERATURE_C }, "Synthetic uniform dark current of 1 e⁻/s per site at 20 °C (the tier presets declare none); dark shot noise follows from it.");
|
|
788
|
+
assume("sensor.spectralResponse", "model", { kind: "colorimetric-cmf-eqe", peakScale: EQE_SCALE }, "Idealized colorimetric sensor: channel EQE = a·{x̄, ȳ, z̄}(λ)·555/λ (the engine's analytic CIE 1931 fit), so RAW channels are proportional to CIE XYZ (Luther–Ives) and the camera matrix is diagonal. Not a real camera's response; CMF air wavelengths are used as vacuum wavelengths (difference about 0.03 %).");
|
|
789
|
+
if (!antiAliasing)
|
|
790
|
+
assume("sensor.fillFactor", "model", 1, "Square sensitive aperture equal to the pixel pitch; no microlens or anti-aliasing filter.");
|
|
791
|
+
else {
|
|
792
|
+
assume("sensor.fillFactor", "model", 1, "Square sensitive aperture equal to the pixel pitch; no microlens.");
|
|
793
|
+
assume("sensor.antiAliasing", "model", { kind: antiAliasing.kind, ...(antiAliasing.axis ? { axis: antiAliasing.axis } : {}),
|
|
794
|
+
...(antiAliasing.separationSites === undefined ? {} : { separationSites: antiAliasing.separationSites, separationMicrometers: antiAliasing.separationSites * pitch }),
|
|
795
|
+
components: antiAliasing.components }, "Optical low-pass (anti-aliasing) filter as a normalized point-splitting kernel in the sensor optical stack (resolveAntiAliasingSpatialKernel): every site integrates the light that would " +
|
|
796
|
+
"otherwise land at its centre minus each offset, weighted. " + (antiAliasing.kind === "point-splitting" ? "Caller-declared components. " :
|
|
797
|
+
"This is the ideal birefringent " + (antiAliasing.kind === "four-spot" ? "two-plate (crossed) splitter: four equal spots at (±d/2, ±d/2), MTF |cos(π·f_x·d)·cos(π·f_y·d)|" :
|
|
798
|
+
"single-plate splitter: two equal spots at ±d/2 along the " + antiAliasing.axis + " axis, MTF |cos(π·f·d)|") + ", d = " + antiAliasing.separationSites + " site(s); first zero at f = 1/(2d). ") +
|
|
799
|
+
"Approximations: the split is the same at every wavelength, field point and polarization (birefringence dispersion and the thickness-dependent displacement are not modelled), the spots are exact points with equal weights for unpolarized light, " +
|
|
800
|
+
"and the filter's transmission, reflections and focus shift are not modelled. Not a measured filter; the tier presets declare none. The scene queries are multiplied by " + antiAliasing.components.length + ".");
|
|
801
|
+
}
|
|
802
|
+
// Shutter (2.0): the electronic rolling-shutter schedule derived from the body's rolling readout declaration.
|
|
803
|
+
let rollingSchedule;
|
|
804
|
+
if (shutterKind === "rolling") {
|
|
805
|
+
const declared = body.rollingReadoutTiming?.profile;
|
|
806
|
+
if (!declared || declared.readoutMode !== "rolling")
|
|
807
|
+
fail("motion.shutter \"rolling\" needs a camera body with a rolling readout timing declaration.");
|
|
808
|
+
const readout = rollingSkewOption === undefined ? declared : { ...declared, spatialSamplingSkewSeconds: { value: rollingSkewOption, unit: "s", evidence: FACADE_EVIDENCE } };
|
|
809
|
+
rollingSchedule = resolveElectronicRollingShutterExposureSchedule({ readout, linkageEvidence: FACADE_EVIDENCE,
|
|
810
|
+
nominalExposureDurationSeconds: { value: shutterSeconds, unit: "s", evidence: FACADE_EVIDENCE } });
|
|
811
|
+
assume("sensor.shutter", "model", { kind: "electronic-rolling", modelId: rollingSchedule.modelId, modelVersion: rollingSchedule.modelVersion,
|
|
812
|
+
lineScanSkewSeconds: rollingSchedule.lineScanSkewSeconds, scanDirection: readout.scanDirectionNative.value }, "Electronic rolling shutter (line reset/read linkage): a site at normalized scan position s integrates over [s·τ, s·τ + T]. " +
|
|
813
|
+
(rollingSkewOption === undefined ? "τ is the body's declared full-frame data-readout skew, applied unchanged across the simulated frame (it is not scaled to the cropped sensor), so a small frame shows the skew of a full-sensor readout." :
|
|
814
|
+
"τ is the caller-declared skew across the simulated frame."));
|
|
815
|
+
}
|
|
816
|
+
else
|
|
817
|
+
assume("sensor.shutter", "model", "global-electronic", "Global electronic shutter: every site integrates the same exposure window.");
|
|
818
|
+
if (rotating)
|
|
819
|
+
assume("motion.angularVelocityRadPerSec", "model", { ...angular }, "Constant camera angular velocity applied to every scene ray at each temporal midpoint of each site's exposure window (engine rotation convention, docs/SENSOR_ENVIRONMENT_QUERY.md); motion blur is the midpoint quadrature over sensor.temporalSampleCount samples.");
|
|
820
|
+
if (lensPsf)
|
|
821
|
+
assume("lens.psf", "model", { kind: "sampled-local" }, "Caller-supplied sampled lens PSF redistributes irradiance through the environment PSF quadrature; its evidence and limits are the caller's.");
|
|
822
|
+
// With lens.diffraction the PSF is recorded as lens.diffraction once its kernels are built.
|
|
823
|
+
else if (diffractionRadiusOption === undefined)
|
|
824
|
+
assume("lens.psf", "model", "not-applied", "No lens PSF or diffraction blur is applied (point optics); the generic tiers' optical data are declared only at f/4.");
|
|
825
|
+
assume("lens.transmission", "model", lens.matchedReference.transmission, "The tier lens's spectrally flat transmission is used at every f-number and focus in the lens capability; the tier declares it only at its f/4, 5 m reference state.");
|
|
826
|
+
if (!vignetting && !lensFieldMapping)
|
|
827
|
+
assume("lens.fieldThroughput", "model", "unity", "No vignetting, distortion, lateral colour or stray light.");
|
|
828
|
+
else {
|
|
829
|
+
if (vignetting)
|
|
830
|
+
assume("lens.vignetting", "model", { kind: "radial-illumination-vignetting", profile: vignetting }, "Caller-declared radial relative illumination 1 + r2·ρ² + r4·ρ⁴ + r6·ρ⁶, applied once as environment field throughput at the physical sensor point before any PSF.");
|
|
831
|
+
else
|
|
832
|
+
assume("lens.fieldThroughput", "model", "unity", "No vignetting, lateral colour or stray light.");
|
|
833
|
+
if (distortion)
|
|
834
|
+
assume("lens.distortion", "model", distortion, "Caller-declared " + distortion.kind + " distortion: each sensor point is inverse-distorted before the scene-ray query and irradiance is multiplied exactly once by the area factor 1/det(J).");
|
|
835
|
+
}
|
|
836
|
+
if (projection === undefined)
|
|
837
|
+
assume("lens.projection", "default", "rectilinear", "Ideal rectilinear (pinhole) projection.");
|
|
838
|
+
else
|
|
839
|
+
assume("lens.projection", "model", projection, "Ideal " + projection + " projection through the environment lens field mapping. At finite focus its scale is the thin-lens image distance (an approximation); the chart is still sized with the rectilinear thin-lens scale, so its edges move with the projection.");
|
|
840
|
+
// White balance
|
|
841
|
+
let whiteBalanceState = null, needsAuto = false;
|
|
842
|
+
if (whiteBalanceOption === "none")
|
|
843
|
+
whiteBalanceState = null;
|
|
844
|
+
else if (whiteBalanceOption === "auto")
|
|
845
|
+
needsAuto = true;
|
|
846
|
+
else if (whiteBalanceOption === "scene-illuminant") {
|
|
847
|
+
const xyz = illuminantXyz(illuminant), camera = XYZ_TO_CAMERA.map((row) => row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]);
|
|
848
|
+
whiteBalanceState = resolveCustomWhiteBalance({ stateId: "facade-wb-scene-illuminant", sampleSet: { imageStateId: "facade-white-diffuser",
|
|
849
|
+
inputDomain: "relative-pre-wb-camera-linear-rgb", samples: [{ sampleId: "white", red: camera[0], green: camera[1], blue: camera[2], weight: 1, clipped: false }] } });
|
|
850
|
+
}
|
|
851
|
+
else if (typeof whiteBalanceOption === "object" && whiteBalanceOption !== null && "kelvin" in whiteBalanceOption) {
|
|
852
|
+
const wb = allowKeys(whiteBalanceOption, "output.whiteBalance", ["kelvin", "tint"]);
|
|
853
|
+
const intent = createColorTemperatureWhiteBalanceIntent({ intentId: "facade-wb-cct", colorTemperatureKelvin: positive(wb.kelvin, "output.whiteBalance.kelvin"),
|
|
854
|
+
tint: wb.tint === undefined ? 0 : wb.tint });
|
|
855
|
+
whiteBalanceState = resolveColorTemperatureWhiteBalance({ stateId: "facade-wb-cct", intent, whiteLocus: "planckian", tintInterpretation: "duv-cie-1960-uv",
|
|
856
|
+
camera: { profileId: IDS.spectral, profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, xyzToCameraRgb: XYZ_TO_CAMERA.map((r) => [...r]) } });
|
|
857
|
+
}
|
|
858
|
+
else if (typeof whiteBalanceOption === "object" && whiteBalanceOption !== null && "channelGains" in whiteBalanceOption) {
|
|
859
|
+
allowKeys(whiteBalanceOption, "output.whiteBalance", ["channelGains"]);
|
|
860
|
+
requireInputObject(whiteBalanceOption.channelGains, "output.whiteBalance.channelGains");
|
|
861
|
+
allowKeys(whiteBalanceOption.channelGains, "output.whiteBalance.channelGains", ["red", "green", "blue"]);
|
|
862
|
+
whiteBalanceState = resolveManualWhiteBalance({ stateId: "facade-wb-manual", channelGains: whiteBalanceOption.channelGains });
|
|
863
|
+
}
|
|
864
|
+
else
|
|
865
|
+
fail("Unknown output.whiteBalance.");
|
|
866
|
+
const intentOf = (state) => (state ? createCaptureWhiteBalanceIntent({ state }) : null);
|
|
867
|
+
const rendering = { schemaVersion: "0.1.0", profileId: "facade-sdr", profileVersion: "1", renderingExposureEv: 0, toneCurve,
|
|
868
|
+
...(outputOptions.toneCurveParameters === undefined ? {} : { toneCurveParameters: outputOptions.toneCurveParameters }),
|
|
869
|
+
gamutHandling: "clip-components", outputDynamicRange: "sdr", transferFunction: "srgb", bitDepth: format === "srgb-16" || (format === "png" && !wideGamut && pngBitDepth === 16) ? 16 : 8, rounding: "nearest-ties-up", dither: "none" };
|
|
870
|
+
if (!linearPath)
|
|
871
|
+
assume("output.rendering", "model", { renderingExposureEv: 0, gamutHandling: "clip-components" }, "Developed linear sRGB/D65 with the RAW white level mapped to display white, no exposure push, out-of-gamut components clipped.");
|
|
872
|
+
else
|
|
873
|
+
assume("output.rendering", "model", { renderingExposureEv: 0, gamutHandling: "clip-components-in-target-gamut", sdrRendererUsed: false }, "The sRGB SDR renderer is not used: the linear development (RAW white level = relative 1.0, no exposure push) is converted to the target encoding; request.development.rendering is validated but only its exposure and tone curve (identity) apply.");
|
|
874
|
+
// Rendering choices are validated before any radiance work, so a bad tone curve fails fast.
|
|
875
|
+
parseSdrRenderingProfile(rendering);
|
|
876
|
+
if (highlightMode !== "clip" && highlightMode !== "reconstruct")
|
|
877
|
+
fail("output.highlightMode must be clip or reconstruct.");
|
|
878
|
+
// Frame profiles
|
|
879
|
+
const evidence = FACADE_EVIDENCE;
|
|
880
|
+
const nativeRaster = { pixelWidth: nativeWidth, pixelHeight: nativeHeight };
|
|
881
|
+
const captureModeProfile = { schemaVersion: "0.1.0", modes: [{ modeId: IDS.mode, evidence, acquisition: { kind: "single-frame" },
|
|
882
|
+
perFrameSampling: { kind: "native-effective-raster" }, reconstructionStages: [], processedImageRaster: { value: nativeRaster, evidence },
|
|
883
|
+
dependencies: ["color-sampling-model"] }] };
|
|
884
|
+
const bindingProfile = { schemaVersion: "0.1.0", bindingId: IDS.binding, colorSamplingProfileId: IDS.cfa, nativeRaster, evidence,
|
|
885
|
+
relationship: { kind: "regular-native-effective-sample-blocks", sitesPerNativeSampleX: 1, sitesPerNativeSampleY: 1, anchor: "shared-native-top-left" } };
|
|
886
|
+
const exposureWindow = rollingSchedule ? createRollingShutterCaptureExposureSchedule(rollingSchedule) :
|
|
887
|
+
{ shutterMechanism: "electronic", nominalExposureDurationSeconds: { value: shutterSeconds, unit: "s", evidence },
|
|
888
|
+
opening: { kind: "simultaneous" }, closing: { kind: "simultaneous" } };
|
|
889
|
+
const buildRawInput = (intent) => ({
|
|
890
|
+
exposure: { captureId: IDS.capture, sceneStateId: IDS.sceneState, sceneTimeSeconds: 0, geometry,
|
|
891
|
+
exposure: { focalLengthMm, aperture: fNumber, shutterSeconds, iso: resolvedIso }, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
892
|
+
noise: { seedUint32: noiseSeed, realizationId: "facade-noise", model: { ...SENSOR_RAW_PRODUCER_NOISE_MODEL } },
|
|
893
|
+
source: { kind: "scene-radiance-source", artifactId: "facade-scene-" + scenePreset, sha256: sourceSha256, dynamicRangeHistory: "no-loss-declared" },
|
|
894
|
+
whiteBalanceIntent: intent, adoptedWhiteXyz: null,
|
|
895
|
+
models: [{ profile: { id: "photivra-simulate-capture", version: SIMULATE_CAPTURE_FACADE_VERSION }, scientificStatus: "approximation", publicEvidenceIds: ["photivra:simulate-capture-facade"] }] },
|
|
896
|
+
frameId: IDS.frame, modeId: IDS.mode, captureModeProfile, colorSamplingProfile, bindingProfile, exposureWindow,
|
|
897
|
+
maximumOutputBytes: nativeWidth * nativeHeight * 7, tileWidth
|
|
898
|
+
});
|
|
899
|
+
const sceneBinding = { sceneStateId: IDS.sceneState, providerSceneId: IDS.scene, evidence };
|
|
900
|
+
// Shared per-site environment pieces (identical objects for every site keep the native task's shared-state fast path).
|
|
901
|
+
const range = { ...FACADE_SPECTRAL_RANGE_NANOMETERS };
|
|
902
|
+
// EQE breakpoints coincide with the quadrature subintervals, so the quadrature is not split further.
|
|
903
|
+
const eqeSpacing = (range.maximum - range.minimum) / Math.ceil((range.maximum - range.minimum) / spectralStep);
|
|
904
|
+
const eqeSamples = (channel) => {
|
|
905
|
+
const out = [];
|
|
906
|
+
for (let nm = range.minimum; nm <= range.maximum + 1e-9; nm += eqeSpacing)
|
|
907
|
+
out.push({ nm, value: EQE_SCALE[channel] * estimateCie1931ColorMatchingFunctions(nm)[CMF_KEY[channel]] * 555 / nm });
|
|
908
|
+
return out;
|
|
909
|
+
};
|
|
910
|
+
const operatingConditions = { temperatureC: 25, incidenceAngleDegreesFromNormal: 0, polarization: "unpolarized" };
|
|
911
|
+
// One single-channel response profile per CFA channel keeps each site's response data small.
|
|
912
|
+
const spectralResponseProfiles = Object.fromEntries(CHANNELS.map((only) => [only, { schemaVersion: "0.1.0", profileId: IDS.spectral, colorSamplingProfileId: IDS.cfa, evidence,
|
|
913
|
+
channels: [only].map((channelId) => ({ channelId, kind: "effective-external-quantum-efficiency", responseScope: "sensor-package-incident-effective-channel-response",
|
|
914
|
+
scientificStatus: "approximation", uncertainty: { kind: "not-quantified", limitation: "Idealized colorimetric response; not a measured sensor." },
|
|
915
|
+
evidence, referenceConditions: operatingConditions, conditionDependence: "not-modeled", externalQuantumEfficiency: sampled(eqeSamples(channelId)) })) }]));
|
|
916
|
+
const apertureArea = pitch * pitch;
|
|
917
|
+
const samplingApertureProfile = { schemaVersion: "0.1.0", profileId: IDS.sampling, colorSamplingProfileId: IDS.cfa, colorSamplingBindingId: IDS.binding, evidence,
|
|
918
|
+
siteCenterLattice: { kind: "regular-rectangular-site-center-lattice", coordinateSystem: "native-sensor-physical", pitchXMicrometers: pitch, pitchYMicrometers: pitch,
|
|
919
|
+
firstSiteCenterFromImagingAreaTopLeftMicrometers: { x: pitch / 2, y: pitch / 2 }, evidence },
|
|
920
|
+
geometricSensitiveAperture: { kind: "uniform-axis-aligned-rectangle", widthMicrometers: pitch, heightMicrometers: pitch, centerOffsetFromSiteCenterMicrometers: { x: 0, y: 0 }, evidence } };
|
|
921
|
+
const stackId = antiAliasing ? IDS.stackAa : IDS.stack;
|
|
922
|
+
const opticalStackProfile = antiAliasing ? { schemaVersion: "0.1.0", profileId: stackId, evidence, effectiveAntiAliasingSpatialResponse: {
|
|
923
|
+
kind: "normalized-point-splitting-kernel", evidence, coordinateSystem: "native-sensor-physical", scope: "field-wavelength-polarization-invariant-approximation",
|
|
924
|
+
components: antiAliasing.components
|
|
925
|
+
} } : { schemaVersion: "0.1.0", profileId: IDS.stack, evidence, effectiveAntiAliasingSpatialResponse: { kind: "absent", evidence } };
|
|
926
|
+
const perChannel = Object.fromEntries(CHANNELS.map((channelId) => [channelId, {
|
|
927
|
+
applicationProfile: { schemaVersion: "0.1.0", profileId: "facade-application-" + channelId, spectralResponseProfileId: IDS.spectral, colorSamplingProfileId: IDS.cfa, channelId,
|
|
928
|
+
samplingApertureProfileId: IDS.sampling, opticalStackProfileId: stackId, evidence, incidentAreaBasis: { kind: "geometric-sensitive-aperture", areaSquareMicrometers: apertureArea },
|
|
929
|
+
spatialResponseModel: { kind: "uniform-over-geometric-sensitive-aperture", scientificStatus: "approximation", evidence, limitation: "Uniform response over the square aperture." },
|
|
930
|
+
referenceConditionPolicy: { kind: "exact-match-required" } },
|
|
931
|
+
operatingRangeProfile: { schemaVersion: "0.1.0", profileId: "facade-linearity-" + channelId, responseApplicationProfileId: "facade-application-" + channelId,
|
|
932
|
+
spectralResponseProfileId: IDS.spectral, colorSamplingProfileId: IDS.cfa, channelId, scientificStatus: "approximation",
|
|
933
|
+
uncertainty: { kind: "not-quantified", limitation: "Declared linear photo response for the idealized sensor." }, evidence,
|
|
934
|
+
inputRange: { kind: "wavelength-integrated-geometric-aperture-radiant-power", unit: "W", minimumInclusive: 0, maximumInclusive: 1 },
|
|
935
|
+
wavelengthApplicability: { wavelengthBasis: "vacuum", minimumNanometers: range.minimum, maximumNanometers: range.maximum, containment: "requested-range-must-be-contained" },
|
|
936
|
+
linearityCriterion: { maximumAbsoluteRelativeDeviation: 0.01 },
|
|
937
|
+
spatialLinearityModel: { kind: "linear-superposition-over-geometric-aperture", scientificStatus: "approximation", evidence, limitation: "Declared linear superposition." },
|
|
938
|
+
spectralInputModel: { kind: "per-spectral-bin", maximumBinWidthNanometers: Math.max(spectralStep, 20), scientificStatus: "approximation", evidence, limitation: "Declared per-bin linearity." },
|
|
939
|
+
referenceConditions: operatingConditions, referenceConditionPolicy: { kind: "exact-match-required" } }
|
|
940
|
+
}]));
|
|
941
|
+
const sourcePlane = { value: "sensor-package-incident", evidence };
|
|
942
|
+
const localExposure = { bindingProfile, exposureWindowInput: { ...exposureWindow, nativeRaster } };
|
|
943
|
+
const sceneBindings = {
|
|
944
|
+
illuminationProfile: { schemaVersion: "0.1.0", profileId: IDS.lights, sceneId: IDS.scene, evidence, sources: [{ sourceId: "illuminant", family: "environment",
|
|
945
|
+
enabled: true, geometry: { kind: "environment" }, magnitude: { kind: "relative-linear-scale", scale: 1, scientificStatus: "approximation",
|
|
946
|
+
limitation: "Absolute level is carried by the provider radiance (" + illuminanceLux + " lux)." },
|
|
947
|
+
spectrum: { kind: "unresolved", limitation: "Spectral radiance is supplied numerically by the provider (" + illuminant.id + ")." },
|
|
948
|
+
temporalBehavior: { kind: "time-invariant" }, evidence }] },
|
|
949
|
+
materialResponseProfile: { schemaVersion: "0.1.0", profileId: IDS.materials, sceneId: IDS.scene, evidence, materials: [{ materialResponseId: "facade-surface", evidence,
|
|
950
|
+
representation: { kind: "spectral-wavelength-preserving-data", dataArtifact: { id: "facade-scene-" + scenePreset, checksumSha256: sourceSha256 },
|
|
951
|
+
wavelengthBasis: "vacuum", wavelengthRangeNanometers: range, scatteringModel: "provider-defined-wavelength-preserving", scientificStatus: "approximation",
|
|
952
|
+
uncertainty: { kind: "not-quantified", limitation: "Analytic Lambertian reflectance." }, wavelengthChangingBehaviorModeled: false, emissionModeled: false } }],
|
|
953
|
+
fluorescenceModeled: false, volumetricMaterialTransportModeled: false, polarizationModeled: false },
|
|
954
|
+
providerProfile: { schemaVersion: "0.1.0", profileId: IDS.provider, sceneId: IDS.scene, illuminationProfileId: IDS.lights, materialResponseProfileId: IDS.materials,
|
|
955
|
+
outputQuantity: "outgoing-spectral-radiance", outputUnit: "W/m^2/sr/nm", scientificStatus: "approximation",
|
|
956
|
+
uncertainty: { kind: "not-quantified", limitation: "Analytic planar scene." },
|
|
957
|
+
fidelity: { spectral: "wavelength-resolved", material: "spectral-data", visibility: "resolved", directTransport: "approximation", indirectTransport: "not-modeled" },
|
|
958
|
+
wavelengthChangingTransportModeled: false, volumetricTransportModeled: false, polarizationModeled: false, evidence, limitations: ["Engine-authored planar chart or caller scene function.",
|
|
959
|
+
...(diffractionObjectSpace ? ["Radiance is the scene radiance convolved with the lens diffraction PSF mapped onto the scene plane (object-space convolution, request.diffraction)."] : [])] }
|
|
960
|
+
};
|
|
961
|
+
const lensId = lens.exposure.profileId;
|
|
962
|
+
const opticsProfile = { schemaVersion: "0.1.0", profileId: lensId + ":facade-transmission", profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, lensProfileId: lensId,
|
|
963
|
+
scientificStatus: "approximation", applicability: { focalLengthMm: { minimum: capabilities.selectedFocalLengthMm, maximum: capabilities.selectedFocalLengthMm },
|
|
964
|
+
nominalFNumber: { minimum: capabilities.aperture.widestAvailableFNumber, maximum: capabilities.aperture.narrowestAvailableFNumber }, focus: { kind: "any" } },
|
|
965
|
+
transmission: { kind: "spectral-transmission", scientificStatus: "approximation", wavelengthBasis: "vacuum",
|
|
966
|
+
samples: { value: [{ wavelengthNanometers: range.minimum, linearTransmissionFactor: lens.matchedReference.transmission },
|
|
967
|
+
{ wavelengthNanometers: range.maximum, linearTransmissionFactor: lens.matchedReference.transmission }], evidence: preset.evidence },
|
|
968
|
+
uncertainty: { kind: "not-quantified", limitation: "Synthetic spectrally flat transmission from the tier preset." } },
|
|
969
|
+
distortionAreaMappingOwnership: "not-applied-by-bridge", psfRedistributionOwnership: "downstream-normalized-energy-redistribution",
|
|
970
|
+
sensorOpticalStackIncluded: false, strayLightIncluded: false, polarizationModeled: false, wavelengthChangingBehaviorModeled: false, volumetricScatteringModeled: false,
|
|
971
|
+
evidence: preset.evidence, limitations: [...lens.limitations, "Flat transmission applied across the lens capability by the capture facade."] };
|
|
972
|
+
const optics = { profile: opticsProfile, focalLengthMm, nominalFNumber: fNumber,
|
|
973
|
+
focus: { kind: "ideal-symmetric-thin-lens", objectDistanceM: focusDistanceM, pupilMagnificationAssumption: "unity" } };
|
|
974
|
+
const motion = { angularVelocityRadPerSec: angular, timeReference: "first-opening-boundary-phase", environmentDirectionConvention: "outgoing-radiance-toward-camera" };
|
|
975
|
+
const fieldThroughput = vignetting ? { kind: "radial-illumination-vignetting", profile: vignetting, evidence,
|
|
976
|
+
limitation: "Caller-declared radial relative illumination applied by the capture facade; not a measured lens." } :
|
|
977
|
+
{ kind: "unity", evidence, limitation: "No vignetting or field throughput loss applied by the facade." };
|
|
978
|
+
const spectralSamplingByChannel = Object.fromEntries(CHANNELS.map((channelId) => [channelId, { channelId, wavelengthBasis: "vacuum", wavelengthRangeNanometers: range,
|
|
979
|
+
maximumSubintervalWidthNanometers: spectralStep }]));
|
|
980
|
+
// Diffraction (2.1): Airy kernels at the resolved f-number on the site pitch, one per spectral node of the sensor quadrature.
|
|
981
|
+
let diffraction, diffractionPsf;
|
|
982
|
+
if (diffractionRadiusOption !== undefined) {
|
|
983
|
+
const wavelengths = [...new Set(CHANNELS.flatMap((channelId) => calculateSensorSpectralQuadrature({ ...spectralSamplingByChannel[channelId], colorSamplingProfile,
|
|
984
|
+
spectralResponseProfile: spectralResponseProfiles[channelId] }).value.nodes.map((node) => node.wavelengthNanometers)))].sort((a, b) => a - b);
|
|
985
|
+
const lambdaN = wavelengths[wavelengths.length - 1] / 1000 * fNumber * imageDistanceMm / focalLengthMm;
|
|
986
|
+
// The environment resolves the PSF at each pre-filter source point, up to the largest split offset outside the imaging area.
|
|
987
|
+
const aaReachMm = antiAliasing ? Math.max(...antiAliasing.components.map((c) => Math.max(Math.abs(c.offsetMicrometers.x), Math.abs(c.offsetMicrometers.y)))) / 1000 : 0;
|
|
988
|
+
const kernelRadiusSamples = diffractionRadiusOption ?? Math.min(32, Math.max(1, Math.ceil(AIRY_THIRD_ZERO_PER_LAMBDA_N * lambdaN / pitch)));
|
|
989
|
+
diffraction = createDiffractionLimitedPsfProfile({ aperture: "circular", focalLengthMm, apertureFNumber: fNumber, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
990
|
+
wavelengthsNm: wavelengths, samplePitchMicrometers: pitch, kernelRadiusSamples, fieldHalfExtentMm: aaReachMm === 0 ? { x: imagingArea.widthMm / 2, y: imagingArea.heightMm / 2 } :
|
|
991
|
+
{ x: imagingArea.widthMm / 2 + aaReachMm, y: imagingArea.heightMm / 2 + aaReachMm },
|
|
992
|
+
profileId: "facade-diffraction-airy" }).value;
|
|
993
|
+
diffractionPsf = { kind: "sampled-local", configuration: { psf: { profile: diffraction.profile, focalLengthMm, focus: { kind: "finite", distanceM: focusDistanceM },
|
|
994
|
+
apertureFNumber: fNumber, signedDefocusImagePlaneMicrometers: 0 }, psfWavelengthBasis: { value: "vacuum", evidence },
|
|
995
|
+
spatialModel: { kind: "destination-local-shift-invariant-approximation", evidence, limitation: "Diffraction-limited Airy PSF, the same at every field point." } } };
|
|
996
|
+
assume("lens.diffraction", "model", { model: diffraction.model, aperture: "circular-aperture", fNumber, workingFNumber: diffraction.workingFNumber,
|
|
997
|
+
kernelRadiusSamples, samplePitchMicrometers: pitch, maximumTruncatedEnergyFraction: diffraction.maximumTruncatedEnergyFraction,
|
|
998
|
+
truncatedEnergyByWavelength: diffraction.wavelengths.map((w) => ({ wavelengthNm: w.wavelengthNm, truncatedEnergyFraction: w.truncatedEnergyFraction })),
|
|
999
|
+
execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" }, "Scalar Fraunhofer diffraction of an ideal, aberration-free circular pupil (Airy pattern, createDiffractionLimitedPsfProfile) at the resolved f-number, in focus, with D = f/N and the thin-lens image distance (working f-number N·v/f). " +
|
|
1000
|
+
"One kernel per spectral node, binned into site-sized cells over a " + (2 * kernelRadiusSamples + 1) + " × " + (2 * kernelRadiusSamples + 1) + " window" +
|
|
1001
|
+
(diffractionRadiusOption === null ? " (reaching the third dark ring at the longest node)" : "") + "; the Airy tail energy outside it (listed per wavelength) is put back by unit-sum renormalization. " +
|
|
1002
|
+
(diffractionObjectSpace ? "Applied as an object-space convolution: the scene radiance on the site lattice is convolved once per spectral node (calculateIrradianceConvolution) and the provider answers each site query with it, so the site inputs declare no PSF; this equals the per-tap environment PSF quadrature for this still, distortion-free planar scene (request.diffraction.psf). " :
|
|
1003
|
+
"Applied through the environment PSF quadrature (one scene query per kernel tap), because camera rotation, a lens field mapping or vignetting moves the taps off the site lattice. ") +
|
|
1004
|
+
"No defocus, aberration or pupil clipping; the sensor pixel aperture is still point-sampled at the site centre.");
|
|
1005
|
+
}
|
|
1006
|
+
const psf = lensPsf ?? (diffractionObjectSpace ? { kind: "not-applied", evidence,
|
|
1007
|
+
limitation: "No environment PSF: lens diffraction is applied upstream by the scene provider as an object-space convolution (request.diffraction)." } : undefined) ??
|
|
1008
|
+
diffractionPsf ?? { kind: "not-applied", evidence, limitation: "No lens PSF (point optics)." };
|
|
1009
|
+
const pupilRecord = pupil ? { kind: "ideal-uniform-circular-pupil", radialSampleCount: pupil.radialSamples, angularSampleCount: pupil.angularSamples, evidence,
|
|
1010
|
+
limitation: "Ideal uniform circular pupil; finite ray sampling, no convergence claim." } : undefined;
|
|
1011
|
+
const chargeSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.chargeSampling, completenessProfileId: IDS.completeness, scientificStatus: "approximation",
|
|
1012
|
+
photoShotNoiseModel: "poisson", darkShotNoiseModel: "poisson", additionalComponentPolicies: [], evidence, limitations: ["Poisson photo and dark shot noise."] };
|
|
1013
|
+
const channelAt = (x, y) => ["red", "green", "green", "blue"][(y % 2) * 2 + (x % 2)];
|
|
1014
|
+
function siteInput(index) {
|
|
1015
|
+
if (!Number.isSafeInteger(index) || index < 0 || index >= nativeWidth * nativeHeight)
|
|
1016
|
+
fail("Native site index out of range.");
|
|
1017
|
+
const x = index % nativeWidth, y = Math.floor(index / nativeWidth), channel = channelAt(x, y), site = { x, y }, c = perChannel[channel], r = regimeByChannel[channel];
|
|
1018
|
+
const environment = { temporalIntegrationId: IDS.frame + ":native:" + index,
|
|
1019
|
+
sensor: { spatialSampling: { imagingArea, samplingApertureProfile, opticalStackProfile, site, spatialSampleCountX: 1, spatialSampleCountY: 1 },
|
|
1020
|
+
spectralSampling: spectralSamplingByChannel[channel], colorSamplingProfile, spectralResponseProfile: spectralResponseProfiles[channel],
|
|
1021
|
+
responseApplication: { applicationProfile: c.applicationProfile, sourcePlane, operatingConditions }, operatingRangeProfile: c.operatingRangeProfile, localExposure },
|
|
1022
|
+
sceneBindings, optics, motion, temporalSampleCount, fieldThroughput, psf, ...(pupilRecord ? { pupil: pupilRecord } : {}), ...(lensFieldMapping ? { lensFieldMapping } : {}) };
|
|
1023
|
+
// The charge completeness profile must cover the site's own exposure window (shifted by s·τ under a rolling shutter).
|
|
1024
|
+
const window = rollingSchedule ? calculateCaptureExposureWindows({ ...exposureWindow, nativeRaster, samplePointsNative: [{ x: x + 0.5, y: y + 0.5 }] }).value.samples[0] :
|
|
1025
|
+
{ startOffsetSecondsFromOpeningReference: 0, endOffsetSecondsFromOpeningReference: shutterSeconds };
|
|
1026
|
+
return { environment, operatingTemperatureC: OPERATING_TEMPERATURE_C,
|
|
1027
|
+
darkCurrentProfile: { schemaVersion: "0.1.0", profileId: IDS.dark, colorSamplingProfileId: IDS.cfa, channelId: channel, scientificStatus: "approximation", evidence,
|
|
1028
|
+
uncertainty: { kind: "not-quantified", limitation: "Synthetic dark current for the capture facade." }, chargeMeaning: "pre-compensation-thermally-generated-electrons",
|
|
1029
|
+
siteApplicability: { kind: "exact-site", site }, temperatureModel: { kind: "fixed-reference-temperature", referenceTemperatureC: OPERATING_TEMPERATURE_C,
|
|
1030
|
+
darkCurrentElectronsPerSecond: DARK_CURRENT_ELECTRONS_PER_SECOND }, darkCurrentCompensationIncluded: false, spatialDarkCurrentNonuniformityModeled: false },
|
|
1031
|
+
charge: { completenessProfile: { schemaVersion: "0.1.0", profileId: IDS.completeness, colorSamplingProfileId: IDS.cfa, channelId: channel, site, bindingId: IDS.binding,
|
|
1032
|
+
timeReference: "first-opening-boundary-phase", startOffsetSecondsFromOpeningReference: window.startOffsetSecondsFromOpeningReference,
|
|
1033
|
+
endOffsetSecondsFromOpeningReference: window.endOffsetSecondsFromOpeningReference,
|
|
1034
|
+
darkCurrentProfileId: IDS.dark, includedAdditionalComponentIds: [], coverageMeaning: "all-material-stored-electron-contributors-accounted-for",
|
|
1035
|
+
scientificStatus: "approximation", evidence, limitation: "Photo and dark charge only." } },
|
|
1036
|
+
readout: { samplingProfile: chargeSamplingProfile, capacityProfile: { schemaVersion: "0.1.0", profileId: IDS.capacity, colorSamplingProfileId: IDS.cfa, channelId: channel,
|
|
1037
|
+
capacityMeaning: "physical-charge-storage-capacity-electrons", capacityElectrons, scientificStatus: "approximation",
|
|
1038
|
+
uncertainty: { kind: "not-quantified", limitation: "Derived from the tier's declared saturation signal." }, evidence, siteApplicability: { kind: "exact-site", site },
|
|
1039
|
+
operatingState: { stateId: IDS.operatingState, evidence }, temperatureApplicability: { kind: "exact-reference-temperature", temperatureC: OPERATING_TEMPERATURE_C } },
|
|
1040
|
+
operatingStateId: IDS.operatingState, readoutProfile: r.readoutProfile, regimeId: r.regimeId } };
|
|
1041
|
+
}
|
|
1042
|
+
const readTile = async (request) => ({ ...request, sites: Array.from({ length: request.width }, (_, j) => siteInput(request.y * nativeWidth + request.x + j)) });
|
|
1043
|
+
const yieldControl = createHostYield().yieldControl;
|
|
1044
|
+
const perSite = queriesPerSite * (diffraction && psf === diffractionPsf ? (2 * diffraction.kernelRadiusSamples + 1) ** 2 : 1);
|
|
1045
|
+
let diffractionMs = 0, demosaicMs, encodeMs;
|
|
1046
|
+
const rawProvider = diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
1047
|
+
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
1048
|
+
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
1049
|
+
// The environment queries each site at its centre minus each split offset (sites, +x right, +y down).
|
|
1050
|
+
...(antiAliasing ? { queryOffsetsSites: antiAliasing.components.map((c) => ({ x: -c.offsetMicrometers.x / pitch, y: -c.offsetMicrometers.y / pitch })) } : {}) }) : provider;
|
|
1051
|
+
if (antiAliasing && perSite > 100_000)
|
|
1052
|
+
fail("sensor.antiAliasing with these options needs " + perSite + " scene queries per site; the environment admits at most 100000.");
|
|
1053
|
+
const evaluations = nativeWidth * nativeHeight * perSite;
|
|
1054
|
+
// The native task admits at most 100 000 radiance queries per tile.
|
|
1055
|
+
const tileWidth = Math.max(1, Math.min(256, Math.floor(100_000 / perSite)));
|
|
1056
|
+
const environmentRaw = { raw: buildRawInput(intentOf(whiteBalanceState)), sceneBinding,
|
|
1057
|
+
maximumProviderEvaluations: Math.min(2_000_000_000, Math.max(nativeWidth * nativeHeight, evaluations)) };
|
|
1058
|
+
async function runTask(task) {
|
|
1059
|
+
const onAbort = () => task.cancel();
|
|
1060
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1061
|
+
try {
|
|
1062
|
+
checkAbort();
|
|
1063
|
+
await task.run();
|
|
1064
|
+
checkAbort();
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
task.dispose();
|
|
1068
|
+
if (signal?.aborted)
|
|
1069
|
+
fail("simulateCapture aborted.");
|
|
1070
|
+
throw error;
|
|
1071
|
+
}
|
|
1072
|
+
finally {
|
|
1073
|
+
signal?.removeEventListener("abort", onAbort);
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
const rawStarted = Date.now();
|
|
1077
|
+
const plan = calculateNativeRawPlan(environmentRaw.raw);
|
|
1078
|
+
const progress = (stage, total) => {
|
|
1079
|
+
let completed = 0;
|
|
1080
|
+
return () => { completed++; onProgress?.({ stage, completedTileCount: completed, totalTileCount: total }); };
|
|
1081
|
+
};
|
|
1082
|
+
const rawTick = progress("raw", plan.tileCount);
|
|
1083
|
+
const rawTask = createNativeEnvironmentRawTask(environmentRaw, {
|
|
1084
|
+
readTile: async (request) => { const tile = await readTile(request); rawTick(); return tile; },
|
|
1085
|
+
evaluateRadiance: rawProvider.evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl
|
|
1086
|
+
});
|
|
1087
|
+
await runTask(rawTask);
|
|
1088
|
+
let rawOutput = rawTask.takeOutput();
|
|
1089
|
+
const providerEvaluationCount = rawOutput.providerEvaluationCount;
|
|
1090
|
+
const rawMs = Date.now() - rawStarted, wbStarted = Date.now();
|
|
1091
|
+
let finalRawInput = environmentRaw;
|
|
1092
|
+
if (needsAuto) {
|
|
1093
|
+
// Grey-world AWB over pre-WB RAW: black-subtracted, white-normalized channel means of each 2×2 CFA block on a coarse grid.
|
|
1094
|
+
const raw = rawOutput.raw, step = Math.max(2, 2 * Math.floor(Math.sqrt(width * height / 4096) / 1)), samples = [];
|
|
1095
|
+
for (let y = margin - (margin % 2); y + 1 < nativeHeight; y += step)
|
|
1096
|
+
for (let x = margin - (margin % 2); x + 1 < nativeWidth; x += step) {
|
|
1097
|
+
const value = (dx, dy) => { const i = (y + dy) * nativeWidth + x + dx; return (raw.codes[i] - raw.blackLevels[i]) / (raw.effectiveSaturationCodes[dy * 2 + dx] - raw.blackLevels[i]); };
|
|
1098
|
+
let clipped = false;
|
|
1099
|
+
for (const [dx, dy] of [[0, 0], [1, 0], [0, 1], [1, 1]])
|
|
1100
|
+
if (raw.saturationFlags[(y + dy) * nativeWidth + x + dx] !== 0)
|
|
1101
|
+
clipped = true;
|
|
1102
|
+
samples.push({ sampleId: "block-" + x + "-" + y, red: Math.max(1e-9, value(0, 0)), green: Math.max(1e-9, (value(1, 0) + value(0, 1)) / 2), blue: Math.max(1e-9, value(1, 1)), weight: 1, clipped });
|
|
1103
|
+
}
|
|
1104
|
+
const policy = body.whiteBalance.awbPolicies[0];
|
|
1105
|
+
assume("output.whiteBalance.auto", "model", { policyId: policy.policyId, samples: samples.length }, "Grey-world AWB of the body's " + policy.policyId + " policy over 2×2 CFA block means of the pre-WB RAW; the RAW is then rendered again with that intent (the same radiance queries are repeated).");
|
|
1106
|
+
whiteBalanceState = estimateAutoWhiteBalance({ stateId: "facade-wb-auto", profile: body.whiteBalance, policyId: policy.policyId,
|
|
1107
|
+
sampleSet: { imageStateId: "facade-pre-wb-raw", inputDomain: "relative-pre-wb-camera-linear-rgb", samples } });
|
|
1108
|
+
finalRawInput = { raw: buildRawInput(intentOf(whiteBalanceState)), sceneBinding, maximumProviderEvaluations: environmentRaw.maximumProviderEvaluations };
|
|
1109
|
+
const wbTick = progress("white-balance", plan.tileCount);
|
|
1110
|
+
// A second lean RAW pass with the measured intent. It gives the same RAW as re-running the sensor stage on a
|
|
1111
|
+
// retained optical stage, and is faster: retaining the optical stage takes the first pass off the lean executor.
|
|
1112
|
+
const wbTask = createNativeEnvironmentRawTask(finalRawInput, {
|
|
1113
|
+
readTile: async (request) => { const tile = await readTile(request); wbTick(); return tile; },
|
|
1114
|
+
evaluateRadiance: rawProvider.evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl
|
|
1115
|
+
});
|
|
1116
|
+
await runTask(wbTask);
|
|
1117
|
+
rawOutput = wbTask.takeOutput();
|
|
1118
|
+
}
|
|
1119
|
+
const wbMs = Date.now() - wbStarted, devStarted = Date.now();
|
|
1120
|
+
const colorProfile = { schemaVersion: "0.1.0", profileId: IDS.spectral, profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, colorSamplingProfileId: IDS.cfa,
|
|
1121
|
+
channelIds: ["red", "green", "blue"], scientificStatus: "approximation", referenceIlluminant: "D65", normalizedCameraChannelsToXyz: CAMERA_TO_XYZ,
|
|
1122
|
+
evidence, limitations: ["Diagonal camera→XYZ matrix of the idealized colorimetric (Luther–Ives) sensor; exact up to spectral quadrature."] };
|
|
1123
|
+
const phaseProfiles = builtInDemosaic ? builtInDemosaic.phaseProfiles : bilinearPhaseProfiles();
|
|
1124
|
+
const developmentWhiteBalance = whiteBalanceState ? "apply-resolved-sensor-gains" : "not-required";
|
|
1125
|
+
// DNG (2.0): the packed native RAW itself, before development takes ownership of it.
|
|
1126
|
+
let dng;
|
|
1127
|
+
if (format === "dng") {
|
|
1128
|
+
const noise = Object.fromEntries(CHANNELS.map((channel) => {
|
|
1129
|
+
const resolved = resolveSensorReadoutRegime({ profile: regimeByChannel[channel].readoutProfile, regimeId: regimeByChannel[channel].regimeId });
|
|
1130
|
+
return [channel, { conversionGainElectronsPerCode: resolved.regime.systemConversionGainElectronsPerCode.value / (resolved.regime.adc.digitalGain ?? 1),
|
|
1131
|
+
readNoiseRmsElectrons: resolved.combinedReadNoiseRmsElectrons }];
|
|
1132
|
+
}));
|
|
1133
|
+
const dngStarted = clockMs();
|
|
1134
|
+
dng = await serializeNativeRawDng({ raw: rawOutput.raw, colorProfile, noise, ...(dngCapturedAtUtc === undefined ? {} : { capturedAtUtc: dngCapturedAtUtc }) });
|
|
1135
|
+
encodeMs = clockMs() - dngStarted;
|
|
1136
|
+
checkAbort();
|
|
1137
|
+
assume("output.dng", "model", { writer: "serializeNativeRawDng", schemaVersion: dng.schemaVersion, whiteLevel: dng.whiteLevel, blackLevels: dng.blackLevels,
|
|
1138
|
+
noiseProfile: dng.noiseProfile, unrepresentedSaturationFlagCount: dng.unrepresentedSaturationFlagCount }, "Lossless uncompressed 16-bit CFA DNG of exactly the packed native RAW codes: per-phase BlackLevel, WhiteLevel = smallest per-phase effective white, ActiveArea and DefaultCrop from the capture geometry, ColorMatrix1/ForwardMatrix1 from the facade's diagonal camera→XYZ profile (D65), AsShotNeutral from the white-balance intent, BaselineExposure 0, and NoiseProfile from each channel's readout regime (conversion gain ÷ digital gain, combined read noise; Poisson + Gaussian + quantization only). No linear master or developed image is declared; saturation flags below WhiteLevel are not representable and are counted.");
|
|
1139
|
+
}
|
|
1140
|
+
// Development
|
|
1141
|
+
const development = { phaseProfiles, colorProfile,
|
|
1142
|
+
whiteBalance: developmentWhiteBalance, rendering, highlightMode, ...(mirrorBorder ? { borderHandling: "mirror" } : {}),
|
|
1143
|
+
maximumRetainedPayloadBytes: plan.outputBytes + width * height * 3 * (rendering.bitDepth / 8) };
|
|
1144
|
+
let developed;
|
|
1145
|
+
let gamutMappingDiagnostics;
|
|
1146
|
+
if (ahd) {
|
|
1147
|
+
let lastYield = Date.now();
|
|
1148
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, async (completed, total) => {
|
|
1149
|
+
onProgress?.({ stage: "development", completedTileCount: completed, totalTileCount: total });
|
|
1150
|
+
if (Date.now() - lastYield >= 8) {
|
|
1151
|
+
await yieldControl();
|
|
1152
|
+
checkAbort();
|
|
1153
|
+
lastYield = Date.now();
|
|
1154
|
+
}
|
|
1155
|
+
}, { yieldControl: () => yieldControl(), checkAbort, onDemosaicMs: (ms) => { demosaicMs = ms; } });
|
|
1156
|
+
if (!linearPath) {
|
|
1157
|
+
// The native development's SDR kernel, row by row, on the AHD-developed linear sRGB.
|
|
1158
|
+
const maximum = 2 ** rendering.bitDepth - 1, tone = prepareSdrPixelTone(rendering), scratch = new Float64Array(3);
|
|
1159
|
+
const codes = rendering.bitDepth === 8 ? new Uint8Array(width * height * 3) : new Uint16Array(width * height * 3);
|
|
1160
|
+
const counts = { negative: 0, above: 0, changed: 0, low: 0, high: 0, outOfGamut: false };
|
|
1161
|
+
for (let y = 0; y < height; y++)
|
|
1162
|
+
renderSdrRunInto(linear.rgb, y * width * 3, width, rendering, tone, 2 ** rendering.renderingExposureEv, maximum, codes, y * width * 3, scratch, counts);
|
|
1163
|
+
developed = { raw: rawOutput.raw, integerSamples: codes, highlight: linear.highlight, diagnostics: { renderingNegativeSampleCount: counts.negative,
|
|
1164
|
+
renderingAboveReferenceSampleCount: counts.above, toneChangedSampleCount: counts.changed, gamutClippedLowSampleCount: counts.low,
|
|
1165
|
+
gamutClippedHighSampleCount: counts.high, captureSaturation: "consumed-in-raw-development" } };
|
|
1166
|
+
}
|
|
1167
|
+
else {
|
|
1168
|
+
const mapping = perceptual ? prepareRgbGamutMapping({ colorSpace: colorEncoding, method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION }) : undefined;
|
|
1169
|
+
const encoded = encodeSdrFromLinear(linear.rgb, colorEncoding, pngBitDepth, mapping);
|
|
1170
|
+
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
1171
|
+
if (mapping)
|
|
1172
|
+
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
1173
|
+
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
else if (!linearPath) {
|
|
1177
|
+
const devTask = createNativeRawDevelopmentTask({ ...development, raw: rawOutput.raw }, { yieldControl,
|
|
1178
|
+
onProgress: (p) => onProgress?.({ stage: "development", completedTileCount: p.completedTileCount, totalTileCount: p.totalTileCount }) });
|
|
1179
|
+
await runTask(devTask);
|
|
1180
|
+
developed = devTask.takeOutput();
|
|
1181
|
+
}
|
|
1182
|
+
else {
|
|
1183
|
+
// Wide gamut: the native development operator up to linear sRGB/D65 (unclipped), then XYZ → target primaries.
|
|
1184
|
+
let lastYield = Date.now();
|
|
1185
|
+
const linear = await developLinearSrgb({ ...development, raw: rawOutput.raw }, async (completed, total) => {
|
|
1186
|
+
onProgress?.({ stage: "development", completedTileCount: completed, totalTileCount: total });
|
|
1187
|
+
if (Date.now() - lastYield >= 8) {
|
|
1188
|
+
await yieldControl();
|
|
1189
|
+
checkAbort();
|
|
1190
|
+
lastYield = Date.now();
|
|
1191
|
+
}
|
|
1192
|
+
});
|
|
1193
|
+
{
|
|
1194
|
+
const mapping = perceptual ? prepareRgbGamutMapping({ colorSpace: colorEncoding, method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION }) : undefined;
|
|
1195
|
+
const encoded = encodeSdrFromLinear(linear.rgb, colorEncoding, pngBitDepth, mapping);
|
|
1196
|
+
developed = { raw: rawOutput.raw, integerSamples: encoded.samples, highlight: linear.highlight, diagnostics: encoded.diagnostics };
|
|
1197
|
+
if (mapping) {
|
|
1198
|
+
gamutMappingDiagnostics = { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION,
|
|
1199
|
+
adjustedPixelCount: encoded.adjustedPixelCount, lightnessClippedPixelCount: encoded.lightnessClippedPixelCount };
|
|
1200
|
+
assume("output.gamutMapping", "model", { method: "cielab-hue-preserving-chroma-compression", kneeFraction: PERCEPTUAL_GAMUT_KNEE_FRACTION, colorSpace: colorEncoding }, "prepareRgbGamutMapping: CIELAB L* and hue held, chroma above 90 % of the boundary chroma C_max(L*, h) compressed with a tanh knee so it never reaches C_max (Morovič 2008, §10). In-gamut colours below the knee are unchanged; saturated in-gamut colours above it are desaturated slightly. L* ≥ 100 (above the RAW white) maps to white. CIELAB hue is not perceptually constant for saturated blues, so they can shift toward purple.");
|
|
1201
|
+
}
|
|
1202
|
+
assume("output.colorEncoding.conversion", "model", { from: "linear-srgb-d65 (unclipped)", via: "CIE XYZ", to: colorEncoding, gamutHandling: perceptual ? "cielab-hue-preserving-chroma-compression" : "clip-components",
|
|
1203
|
+
transfer: colorEncoding === "rec2020" ? "bt2020-oetf (= BT.709 OETF)" : "srgb (W3C CSS Color 4)" }, "Developed with the native development operator (demosaic, per-site white-level clip, WB, highlight mode, diagonal camera→XYZ, XYZ→linear sRGB) but before any sRGB clipping, so colours outside sRGB are kept; converted to " +
|
|
1204
|
+
colorEncoding + " through XYZ (matrices from primaries and D65, as in W3C CSS Color 4), " + (perceptual ? "gamut-mapped perceptually (output.gamutMapping)" : "components clipped to [0, 1] in the target gamut") +
|
|
1205
|
+
", then encoded and rounded to nearest. RAW white maps to display white, as in the sRGB path.");
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const devMs = Date.now() - devStarted, jpegStarted = Date.now();
|
|
1209
|
+
let jpeg;
|
|
1210
|
+
if (format === "jpeg") {
|
|
1211
|
+
const samples = developed.integerSamples;
|
|
1212
|
+
const jpegSha = await sha256Hex(Array.from(samples.subarray(0, Math.min(samples.length, 4096))).join(",") + ":" + samples.length + ":" + sourceSha256);
|
|
1213
|
+
const source = { captureId: IDS.capture, imageStateId: "facade-developed", artifactId: "facade-developed-srgb8", sha256: jpegSha, width, height, encoding: "encoded-srgb-8-rgb" };
|
|
1214
|
+
// The encoder reads every pixel once, in source tiles of at most 256 × 32 aligned to the crop origin.
|
|
1215
|
+
const reads = Math.ceil(height / 32) * Math.ceil(width / 256), jpegTick = progress("jpeg", reads);
|
|
1216
|
+
const jpegTask = createPrintJpegTask({ source, crop: { x: 0, y: 0, width, height }, outputWidth: width, outputHeight: height, quantizationStep: jpegStep,
|
|
1217
|
+
maximumEncodedBytes: Math.max(65536, width * height * 6), maximumProviderReads: reads }, {
|
|
1218
|
+
readTile: async (request) => {
|
|
1219
|
+
jpegTick();
|
|
1220
|
+
const out = new Uint8Array(request.width * request.height * 3);
|
|
1221
|
+
for (let row = 0; row < request.height; row++)
|
|
1222
|
+
out.set(samples.subarray(((request.y + row) * width + request.x) * 3, ((request.y + row) * width + request.x + request.width) * 3), row * request.width * 3);
|
|
1223
|
+
return { ...request, samples: out };
|
|
1224
|
+
}, yieldControl
|
|
1225
|
+
});
|
|
1226
|
+
const jpegEncodeStarted = clockMs();
|
|
1227
|
+
await runTask(jpegTask);
|
|
1228
|
+
jpeg = jpegTask.takeOutput().bytes;
|
|
1229
|
+
encodeMs = clockMs() - jpegEncodeStarted;
|
|
1230
|
+
assume("output.jpeg", "model", { encoder: "photivra-print-jpeg", iccProfile: "photivra-srgb" }, "Baseline JPEG from the existing print JPEG encoder with an embedded sRGB ICC profile; the source digest covers a sample prefix, length and scene digest.");
|
|
1231
|
+
}
|
|
1232
|
+
const jpegMs = Date.now() - jpegStarted;
|
|
1233
|
+
let png;
|
|
1234
|
+
if (format === "png") {
|
|
1235
|
+
const pngStarted = clockMs();
|
|
1236
|
+
png = serializePngImage({ width, height, bitDepth: pngBitDepth, samples: developed.integerSamples, colourEncoding: pngColourEncoding(colorEncoding) });
|
|
1237
|
+
encodeMs = clockMs() - pngStarted;
|
|
1238
|
+
assume("output.png", "model", { encoder: "serializePngImage", bitDepth: pngBitDepth, colourEncoding: pngColourEncoding(colorEncoding) }, "Deterministic PNG (8/16-bit RGB, in-house zlib) with the colour encoding signalled in the file: the sRGB chunk for sRGB, otherwise a cICP chunk with ITU-T H.273 primaries/transfer code points. No mastering-display (mDCV) or content-light-level (cLLI) metadata is written.");
|
|
1239
|
+
}
|
|
1240
|
+
const ranOptions = new Set([...(pupil ? ["lens.pupilSampling"] : []), ...(projection !== undefined ? ["lens.projection"] : []),
|
|
1241
|
+
...(distortion ? ["lens.distortion"] : []), ...(vignetting ? ["lens.vignetting"] : []), ...(lensPsf ? ["lens.psf"] : []),
|
|
1242
|
+
...(diffraction ? ["lens.diffraction"] : []), ...(antiAliasing ? ["sensor.antiAliasing"] : []),
|
|
1243
|
+
...(temporalSampleCount > 1 ? ["sensor.temporalSampleCount"] : []), ...(rotating ? ["motion.rotation"] : []),
|
|
1244
|
+
...(rollingSchedule ? ["motion.shutter.rolling"] : []), ...(mirrorBorder ? ["output.borderHandling.mirror"] : []),
|
|
1245
|
+
...(needsAuto ? ["output.whiteBalance.auto"] : []), ...(ahd ? ["output.demosaic.ahd"] : []),
|
|
1246
|
+
...(highlightMode === "reconstruct" ? ["output.highlightMode.reconstruct"] : []), ...(wideGamut ? ["output.colorEncoding.wide-gamut"] : []),
|
|
1247
|
+
...(perceptual ? ["output.gamutMapping.perceptual"] : []), ...(format === "jpeg" || format === "png" || format === "dng" ? ["output.format." + format] : [])]);
|
|
1248
|
+
return Object.freeze({
|
|
1249
|
+
facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
1250
|
+
image: { width, height, format, samples: developed.integerSamples, ...(jpeg ? { jpeg } : {}), ...(png ? { png, colorEncoding: colorEncoding } : {}), ...(dng ? { dng: dng.bytes } : {}) },
|
|
1251
|
+
raw: developed.raw, plan,
|
|
1252
|
+
request: { environmentRaw: finalRawInput, siteInput, development, whiteBalanceState,
|
|
1253
|
+
...(diffraction ? { diffraction: { profile: diffraction, psf: diffractionPsf, execution: diffractionObjectSpace ? "object-space-convolution" : "environment-psf-quadrature" } } : {}) },
|
|
1254
|
+
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds),
|
|
1255
|
+
meteredSceneLuminanceCdPerSquareMeter: meteredLuminance, resolution },
|
|
1256
|
+
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
1257
|
+
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
1258
|
+
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, sourceSha256 },
|
|
1259
|
+
diagnostics: { providerEvaluationCount, highlight: developed.highlight, rendering: developed.diagnostics, ...(gamutMappingDiagnostics ? { gamutMapping: gamutMappingDiagnostics } : {}),
|
|
1260
|
+
timingsMs: { raw: rawMs, whiteBalance: wbMs, development: devMs, jpeg: jpegMs, total: Date.now() - started,
|
|
1261
|
+
...(diffraction && diffractionObjectSpace ? { diffraction: diffractionMs } : {}), ...(demosaicMs === undefined ? {} : { demosaic: demosaicMs }),
|
|
1262
|
+
...(encodeMs === undefined ? {} : { encode: encodeMs }) },
|
|
1263
|
+
options: Object.freeze(SIMULATE_CAPTURE_COST_OPTIONS.filter((option) => ranOptions.has(option))) },
|
|
1264
|
+
provenance: { scientificStatus: "approximation", cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion,
|
|
1265
|
+
evidence: [...FACADE_EVIDENCE, ...preset.evidence],
|
|
1266
|
+
composedContracts: ["resolveGenericEquipmentExposureCapabilities", "meterRelativeExposure", "createExposureMeterTargetFromMeteringResult",
|
|
1267
|
+
"resolve" + (mode === "manual" ? "Manual" : mode === "aperture-priority" ? "AperturePriority" : mode === "shutter-priority" ? "ShutterPriority" : "ProgramAuto") + "ExposureMode",
|
|
1268
|
+
...(rollingSchedule ? ["resolveElectronicRollingShutterExposureSchedule", "createRollingShutterCaptureExposureSchedule"] : []),
|
|
1269
|
+
...(diffraction ? ["createDiffractionLimitedPsfProfile", ...(diffractionObjectSpace ? ["resolveLensPsfConvolutionKernels", "calculateIrradianceConvolution"] : [])] : []),
|
|
1270
|
+
...(antiAliasing ? ["resolveAntiAliasingSpatialKernel"] : []), "resolveGenericIsoSignalChain", "createNativeEnvironmentRawTask", ...(needsAuto ? ["estimateAutoWhiteBalance"] : []),
|
|
1271
|
+
...(builtInDemosaic ? ["createBuiltInBayerDemosaicProfiles"] : []), ...(ahd ? ["createAhdDemosaicTask"] : []),
|
|
1272
|
+
...(ahd ? ["native RAW development operator (colour, after AHD)", ...(linearPath ? ["resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : ["SDR rendering kernel"])] : linearPath ? ["native RAW development operator (linear, pre-clip)", "resolveRgbColorSpace", ...(perceptual ? ["prepareRgbGamutMapping"] : [])] : ["createNativeRawDevelopmentTask"]),
|
|
1273
|
+
...(jpeg ? ["createPrintJpegTask"] : []), ...(png ? ["serializePngImage"] : []), ...(dng ? ["resolveSensorReadoutRegime", "serializeNativeRawDng"] : [])],
|
|
1274
|
+
limitations: ["Educational approximation: synthetic tier sensor values, idealized colorimetric sensor, planar Lambertian scene and point optics; not a calibrated camera emulation.",
|
|
1275
|
+
"Not offered by the facade: atmospheric haze and polarizers (the scene-radiance provider contract declares no volumetric transport or polarization), flash, video, HDR brackets and processing operators; compose those APIs directly."] },
|
|
1276
|
+
assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a)))
|
|
1277
|
+
});
|
|
1278
|
+
}
|
|
1279
|
+
/** Chart preset ids accepted by `simulateCapture({ scene: { preset } })`. */
|
|
1280
|
+
export const SIMULATE_CAPTURE_CHART_PRESETS = CHART_SCENE_PRESETS;
|
|
1281
|
+
//# sourceMappingURL=simulate-capture.js.map
|