@photivra/engine 2.15.0 → 2.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +47 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +14 -0
- package/dist/capture/native-environment-lean.js +1 -1
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-raw-development.d.ts +15 -0
- package/dist/capture/native-raw-development.js +12 -6
- package/dist/capture/native-raw-development.js.map +1 -1
- package/dist/capture/raw-development-kernel.d.ts +10 -3
- package/dist/capture/raw-development-kernel.js +25 -6
- package/dist/capture/raw-development-kernel.js.map +1 -1
- package/dist/core/internal-radiance-evaluator.d.ts +5 -2
- package/dist/core/internal-radiance-evaluator.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +109 -0
- package/dist/facade/defocus-image-space-provider.js +359 -0
- package/dist/facade/defocus-image-space-provider.js.map +1 -0
- package/dist/facade/diffraction-provider.d.ts +16 -0
- package/dist/facade/diffraction-provider.js +3 -3
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +21 -0
- package/dist/facade/display-view.js +66 -8
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +111 -2
- package/dist/facade/simulate-capture.js +220 -44
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +7 -3
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/display-view-quality-gpu.d.ts +2 -0
- package/dist/gpu/display-view-quality-gpu.js +57 -13
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +2 -0
- package/dist/gpu/display-view-speed-gpu.js +45 -16
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/image-kernel-accelerator.js +3 -0
- package/dist/gpu/image-kernel-accelerator.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +4 -0
- package/dist/gpu/wgsl-display-view-quality-kernel.js +53 -2
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +15 -0
- package/dist/gpu/wgsl-display-view-speed-kernel.js +42 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/optics/defocus-disc-kernel.d.ts +3 -2
- package/dist/optics/defocus-disc-kernel.js +3 -2
- package/dist/optics/defocus-disc-kernel.js.map +1 -1
- package/dist/optics/sensor-environment-query.d.ts +31 -0
- package/dist/optics/sensor-environment-query.js +45 -13
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/processing/development-detail.d.ts +132 -0
- package/dist/processing/development-detail.js +325 -0
- package/dist/processing/development-detail.js.map +1 -0
- package/docs/API_REFERENCE.md +6 -1
- package/docs/QUICK_START.md +190 -12
- package/docs/USAGE.md +14 -3
- package/docs/api/capture-native-raw-development.md +16 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +27 -2
- package/docs/api/facade-simulate-capture.md +128 -3
- package/docs/api/optics-defocus-disc-kernel.md +3 -2
- package/package.json +1 -1
package/docs/QUICK_START.md
CHANGED
|
@@ -99,6 +99,9 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
99
99
|
| `output.toneCurve` | `"identity"` | Any `SdrToneCurve`, for example `"positive-reinhard-per-channel"`. |
|
|
100
100
|
| `output.toneCurveParameters` | none | Required by the luminance curves (`extended-reinhard-luminance`, `naka-rushton-luminance`); the facade invents no curve parameters. |
|
|
101
101
|
| `output.highlightMode` | `"clip"` | Or `"reconstruct"`. |
|
|
102
|
+
| `output.exposureCompensationStops` | 0 | #556: a development exposure push, −2 to +5 stops, on the RAW before white balance (see [Development exposure push](#development-exposure-push-556)). |
|
|
103
|
+
| `output.diffractionCorrection` | off | #552: `{ wavelengthNm?, regularization?, maximumGain?, kernelRadiusPixels? }`, a bounded deconvolution of the diffraction blur; needs `lens.diffraction`. It never restores detail beyond the diffraction cut-off (see [Sharpening and diffraction correction](#sharpening-and-diffraction-correction-552)). |
|
|
104
|
+
| `output.sharpening` | off | #552: `{ radiusPixels?, amount?, threshold? }`, an unsharp mask after the tone curve; sRGB output only (see [Sharpening and diffraction correction](#sharpening-and-diffraction-correction-552)). |
|
|
102
105
|
| `output.whiteBalance` | `"scene-illuminant"` | Also `"auto"`, `"none"`, `{ kelvin, tint? }` and `{ channelGains }`. |
|
|
103
106
|
| `output.jpegQuantizationStep` | 2 | JPEG only: constant step 1–255, not a manufacturer quality scale. |
|
|
104
107
|
| `sensor.pixelPitchMicrometers` | 6 | 0.5–1000 (2.5; 0.5–100 before). The sensor is `width × pitch` wide (see [Geometry](#geometry)); the ISO readout scales with the pixel area (see [ISO and pixel size](#iso-and-pixel-size)). Above 100 µm the capture is a downsized view of a whole frame (see [Whole-frame views](#whole-frame-views-25)). |
|
|
@@ -631,6 +634,41 @@ with diffraction, the four-spot filter and AHD.
|
|
|
631
634
|
| `{ kelvin, tint? }` | A colour-temperature setting on the Planckian locus (tint as Duv). |
|
|
632
635
|
| `{ channelGains }` | Manual channel gains. |
|
|
633
636
|
|
|
637
|
+
## Development exposure push (#556)
|
|
638
|
+
|
|
639
|
+
`output.exposureCompensationStops` brightens (or darkens) the same RAW in development, like a raw editor's exposure
|
|
640
|
+
slider: underexpose, then push the shadows up. It takes −2 to +5 stops (`NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS`),
|
|
641
|
+
whole or fractional; the default 0 gives byte-identical results.
|
|
642
|
+
|
|
643
|
+
```ts
|
|
644
|
+
import { simulateCapture } from "@photivra/engine";
|
|
645
|
+
|
|
646
|
+
// Three stops under (1/1000 s instead of 1/125 s), brightened three stops in development.
|
|
647
|
+
const pushed = await simulateCapture({ scene: { preset: "uniform" }, exposure: { shutterSeconds: 1 / 1000 },
|
|
648
|
+
output: { width: 64, height: 64, exposureCompensationStops: 3 } });
|
|
649
|
+
console.log(pushed.assumptions.find((a) => a.id === "output.exposureCompensationStops")?.value); // { stops: 3, gain: 8, ... }
|
|
650
|
+
```
|
|
651
|
+
|
|
652
|
+
- **What it does.** Each site's black-subtracted, white-normalized value (code − black)/(white − black) is multiplied
|
|
653
|
+
by 2^stops before demosaic, white balance, the highlight mode, colour, the tone curve and encoding. It is a
|
|
654
|
+
development setting, not a capture exposure: no photons are added.
|
|
655
|
+
- **Shadows keep their noise.** The pushed image shows the RAW's own shot, dark and read noise and its ADC quantization
|
|
656
|
+
steps, scaled by the gain. A sensor with less read noise or more bits per sample recovers shadows more cleanly: a
|
|
657
|
+
pushed gradient shows 12-bit steps that 14- and 16-bit RAWs do not. A +3-stop push of a 3-stop underexposure has the
|
|
658
|
+
normal exposure's mean, with about √8 times its shot noise.
|
|
659
|
+
- **Highlights clip.** A site pushed to or past white clips at white and counts in
|
|
660
|
+
`diagnostics.highlight.saturatedPixelCount`; `highlightMode: "reconstruct"` rolls those pixels off toward neutral.
|
|
661
|
+
Nothing above white is recovered.
|
|
662
|
+
- **Where it applies.** The developed samples (`image.samples`, also with format `"dng"`), JPEG, PNG (every encoding),
|
|
663
|
+
both demosaics, `simulateIdealReference` and `simulateDisplayView` (its per-site readout moments are scaled by the
|
|
664
|
+
gain) honour it. The RAW codes, `result.raw` and the DNG are the unmodified RAW. `output.whiteBalance: "auto"` is
|
|
665
|
+
measured on the unpushed RAW. `request.development.exposureCompensationStops` carries a non-zero push, so
|
|
666
|
+
`createNativeRawDevelopmentTask` replays it.
|
|
667
|
+
- **Reported** in the assumption `output.exposureCompensationStops` (`{ stops, gain, appliedTo, dng }`), only when the
|
|
668
|
+
option is given.
|
|
669
|
+
- **Not the same as `renderingExposureEv`.** The SDR rendering profile's exposure scales the developed colour after the
|
|
670
|
+
highlight clip, so it cannot clip a pushed site; the facade keeps it at 0.
|
|
671
|
+
|
|
634
672
|
## Modelling choices
|
|
635
673
|
|
|
636
674
|
These choices appear in `assumptions` as `kind: "model"` entries:
|
|
@@ -644,7 +682,11 @@ These choices appear in `assumptions` as `kind: "model"` entries:
|
|
|
644
682
|
lens capability. By default no PSF (point-spread function) or diffraction, vignetting, distortion,
|
|
645
683
|
lateral colour or stray light is applied. The tier's optical data exist only at f/4 and 5 m, and
|
|
646
684
|
the facade does not extrapolate them. Vignetting, distortion and PSF are applied only from profiles
|
|
647
|
-
you supply; diffraction of an ideal lens only with `lens.diffraction` (2.1)
|
|
685
|
+
you supply; diffraction of an ideal lens only with `lens.diffraction` (2.1); defocus only with
|
|
686
|
+
`lens.defocus`, as the geometric thin-lens disc (an approximation). With camera rotation, a projection,
|
|
687
|
+
distortion or vignetting the disc is applied in image space with the same diameter at every field point,
|
|
688
|
+
a further approximation whose bounds the `lens.defocus` assumption records (`fieldVariation`, #548; see
|
|
689
|
+
[Optics](#optics)).
|
|
648
690
|
- **Sensor.** Square pixels with a fill factor of 1 and no anti-aliasing filter. A global
|
|
649
691
|
electronic shutter unless `motion.shutter` is `"rolling"`. A synthetic uniform dark current of
|
|
650
692
|
1 e⁻/s at 20 °C (the tiers declare none). The charge capacity equals the body's base-ISO saturation signal,
|
|
@@ -655,8 +697,9 @@ These choices appear in `assumptions` as `kind: "model"` entries:
|
|
|
655
697
|
false-colour artefacts; `"ahd"` and `lens.diffraction` reduce them.
|
|
656
698
|
- **Scene.** A planar Lambertian surface: radiance = reflectance × irradiance / π, with illuminance
|
|
657
699
|
from K_m = 683 lm/W and the analytic ȳ. No shadows, inter-reflection, gloss or fluorescence.
|
|
658
|
-
- **Rendering.** Linear sRGB with RAW white at display white, no exposure push
|
|
659
|
-
components clipped, then the sRGB
|
|
700
|
+
- **Rendering.** Linear sRGB with RAW white at display white, no exposure push (unless
|
|
701
|
+
`output.exposureCompensationStops` asks for one on the RAW), out-of-gamut components clipped, then the sRGB
|
|
702
|
+
transfer function.
|
|
660
703
|
|
|
661
704
|
## Output formats
|
|
662
705
|
|
|
@@ -807,6 +850,46 @@ Why these containers:
|
|
|
807
850
|
Pillow 12.3 and ImageMagick 6.9 read the PNG text chunks and the JPEG comment, and Python's XML parser
|
|
808
851
|
reads both XMP packets.
|
|
809
852
|
|
|
853
|
+
## Sharpening and diffraction correction (#552)
|
|
854
|
+
|
|
855
|
+
Two development stages, both off by default. Without them a request takes the same path and gives the same bytes as before.
|
|
856
|
+
|
|
857
|
+
```ts
|
|
858
|
+
import { simulateCapture } from "@photivra/engine";
|
|
859
|
+
|
|
860
|
+
const result = await simulateCapture({
|
|
861
|
+
scene: { preset: "slanted-edge" },
|
|
862
|
+
lens: { fNumber: 16, diffraction: "circular-aperture" },
|
|
863
|
+
output: { width: 16, height: 16, format: "jpeg", diffractionCorrection: {}, sharpening: { radiusPixels: 1, amount: 0.5 } }
|
|
864
|
+
});
|
|
865
|
+
// The value holds cutoffCyclesPerPixel (the diffraction cut-off) and recoversBeyondCutoff: false.
|
|
866
|
+
console.log(result.assumptions.find((a) => a.id === "output.diffractionCorrection")?.value);
|
|
867
|
+
console.log(result.diagnostics.detail); // { diffractionCorrection: {...}, sharpening: {...} }
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
- **Order:** development exposure push on the RAW (`output.exposureCompensationStops`, when given) → demosaic → colour and
|
|
871
|
+
white balance → **diffraction correction** (linear) → rendering exposure and tone curve → gamut handling and sRGB transfer →
|
|
872
|
+
**unsharp mask** → quantization. The `output.developmentOrder` assumption lists it. A push clips at white before the
|
|
873
|
+
correction, on this path as on the default one.
|
|
874
|
+
- **`output.diffractionCorrection`** needs `lens.diffraction`. It deconvolves the developed linear colour with the
|
|
875
|
+
diffraction OTF of the ideal circular pupil at the capture's working f-number, through a bounded Wiener gain
|
|
876
|
+
min(maximumGain, H(1 + α)/(H² + α)) built as a (2R + 1)² filter. Defaults: 550 nm, α = 0.01, gain bound 4, R = the Airy
|
|
877
|
+
third dark ring in pixels (1–16). The OTF is zero beyond the cut-off 1/(λ·N_w): that detail never reached the sensor and
|
|
878
|
+
stays lost. Below it, contrast is only partly restored and noise is amplified by up to the reported `responsePeakGain`.
|
|
879
|
+
- **`output.sharpening`** is an unsharp mask on the sRGB-encoded values: y = x + amount·(x − G_σ∗x) where the difference is at
|
|
880
|
+
least `threshold`. Defaults: σ = 1 pixel, amount 0.5, threshold 0. It creates halos on both sides of an edge and sharpens
|
|
881
|
+
noise; it adds no detail. Not with a wide-gamut `output.colorEncoding` or `output.gamutMapping: "perceptual"`.
|
|
882
|
+
- **Reported:** the `output.diffractionCorrection` and `output.sharpening` assumptions (settings, cut-off, measured filter
|
|
883
|
+
response, defaults used), `diagnostics.detail` (ringing, clipping and halo counts) and `diagnostics.options`.
|
|
884
|
+
- **Not combinable** with `output.region`; not offered by `simulateDisplayView` or `simulateIdealReference`. The DNG holds the
|
|
885
|
+
RAW codes and is unchanged; only `image.samples` (and the JPEG or PNG made from them) change.
|
|
886
|
+
- **Cost:** both stages hold the whole developed image, as AHD does. At 256 × 256 on an Apple M5 Pro (Node.js 24.19.0) the
|
|
887
|
+
development took 8 ms by default, 15 ms with sharpening and 18–20 ms with diffraction correction (R = 3–5), one run each.
|
|
888
|
+
|
|
889
|
+
The trade-offs of these and the lens corrections (corner resolution after distortion correction, corner noise after
|
|
890
|
+
vignetting correction, residual longitudinal CA, halo overshoot, nothing beyond the cut-off) are measured in
|
|
891
|
+
[Digital optics foundation](https://github.com/Photivra/photivra/blob/main/docs/DIGITAL_OPTICS_FOUNDATION.md#sharpening-diffraction-correction-and-their-trade-offs-552).
|
|
892
|
+
|
|
810
893
|
## Demosaic
|
|
811
894
|
|
|
812
895
|
`output.demosaic` selects:
|
|
@@ -907,7 +990,7 @@ The optics options feed only what the environment site contract accepts:
|
|
|
907
990
|
is sampled at its centre). It cannot be
|
|
908
991
|
combined with `lens.psf` or `lens.pupilSampling`.
|
|
909
992
|
|
|
910
|
-
- `lens.defocus: { model: "geometric-disc" }` (#492; the capture
|
|
993
|
+
- `lens.defocus: { model: "geometric-disc" }` (#492; the capture and the display view render it) models defocus of the flat scene plane when
|
|
911
994
|
`lens.focusDistanceM` differs from `subjectDistanceM`.
|
|
912
995
|
- The model is a geometric approximation for an ideal uniform circular pupil. Each in-focus image point spreads
|
|
913
996
|
into a uniform disc of diameter c = f²·|s − d| / (N·d·(s − f)) on the sensor (`calculateDefocusCircle`, thin-lens
|
|
@@ -953,9 +1036,88 @@ The optics options feed only what the environment site contract accepts:
|
|
|
953
1036
|
[Display view, Defocus](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#defocus).
|
|
954
1037
|
- In focus (c = 0) no kernel is applied, and the image is byte-identical to the same request without the option.
|
|
955
1038
|
`simulateIdealReference`, which has no camera effects, only records the disc.
|
|
956
|
-
- **
|
|
957
|
-
of these moves the scene points that a site sees off the
|
|
958
|
-
|
|
1039
|
+
- **With camera rotation, `lens.projection`, `lens.distortion` or `lens.vignetting` (#548)** the capture applies the
|
|
1040
|
+
disc in image space instead. Each of these moves the scene points that a site sees off the plane lattice (or, for
|
|
1041
|
+
vignetting, gives each point its own radiance-to-irradiance factor), so the object-space convolution does not apply.
|
|
1042
|
+
- **Method.** For each spectral node, the sensor-plane irradiance before any PSF is formed on the sensor's site
|
|
1043
|
+
lattice: each lattice point's scene ray through the inverse distortion, the declared projection and (with rotation)
|
|
1044
|
+
the camera's orientation at that instant, times its field factor (vignetting throughput × distortion area factor
|
|
1045
|
+
1/det J). That image is convolved with the disc, and each site query is answered with the result divided by the
|
|
1046
|
+
queried point's own field factor, which the environment multiplies back. This is the environment PSF quadrature with
|
|
1047
|
+
the disc as a sampled PSF (one scene query per kernel tap), summed in another order: a test checks it to 1e-12 per
|
|
1048
|
+
site in expected electrons, and the RAW codes equal those of the same request with the disc passed as `lens.psf`.
|
|
1049
|
+
With the identity mapping (`lens.projection: "rectilinear"`, no distortion) it gives the object-space path's RAW.
|
|
1050
|
+
- **Rotation.** Each environment temporal midpoint is a still camera, so the disc is applied to that instant's
|
|
1051
|
+
image in that instant's frame, then the midpoints are summed as before.
|
|
1052
|
+
- **Vignetting** is applied at each scene point before the disc spreads its light, as for any environment PSF. No
|
|
1053
|
+
multiplier is moved past the blur, so there is no commutation error to bound.
|
|
1054
|
+
The disc does not take the cat's-eye shape of mechanical (pupil) vignetting; the vignetting profile models
|
|
1055
|
+
illumination falloff only.
|
|
1056
|
+
- **With `lens.diffraction`** each node's diffraction kernel convolved with the disc is the image-space kernel, and
|
|
1057
|
+
`request.diffraction.execution` and the `lens.diffraction` assumption say `"image-space-convolution"`.
|
|
1058
|
+
- **What is approximate**, beyond the disc itself: the disc has the same diameter at every field point, centred on
|
|
1059
|
+
each point's chief ray. For the rectilinear, distortion-free thin lens that is exact (a fronto-parallel plane is
|
|
1060
|
+
imaged onto a plane, and the cone from the circular pupil cuts the sensor in a circle of the same size everywhere).
|
|
1061
|
+
For the other cases the `lens.defocus` assumption's `fieldVariation` records a bound for the request, evaluated on
|
|
1062
|
+
a 9 × 9 grid of sensor points over the frame (edges included):
|
|
1063
|
+
- `rotationMaximumDiameterChangeSites`: pitch or yaw tilts the scene plane in the rotated camera, so the disc of a
|
|
1064
|
+
scene point's axial depth there differs from the one used; the largest change over the rendered instants, in
|
|
1065
|
+
sites (0 for roll alone).
|
|
1066
|
+
- `distortionMaximumLocalScaleDeviation`: the largest departure from 1 of the distortion's local scale (singular
|
|
1067
|
+
values of d distorted / d ideal), the relative disc-size difference to a model that blurs the undistorted image
|
|
1068
|
+
and then distorts it. Which model a real lens follows depends on where its distortion arises; the first-order
|
|
1069
|
+
argument (the image-side cone is bounded by the exit pupil, which distortion does not change) favours the
|
|
1070
|
+
constant disc used here.
|
|
1071
|
+
- `projectionMaximumDiameterChangeSites`: for a fisheye or equirectangular projection, the disc of the distance
|
|
1072
|
+
along each scene point's ray instead of its axial depth. The engine has no pupil model for these projections
|
|
1073
|
+
(`lens.pupilSampling` refuses them; a traced reference waits for the 3D ray provider, #358), so a real lens may lie
|
|
1074
|
+
anywhere between the two.
|
|
1075
|
+
- **Refused:** camera rotation with `motion.shutter: "rolling"` (each row has its own instants, so each row would
|
|
1076
|
+
need its own image of the 2R + 1 rows the disc reaches), and a vignetting or distortion profile that does not
|
|
1077
|
+
reach the frame's corners plus the kernel radius (the message names the reach). `lens.pupilSampling` stays refused
|
|
1078
|
+
with projection and distortion. `simulateDisplayView` and `prepareDisplayView` refuse rotation, projection,
|
|
1079
|
+
distortion and vignetting in any focus (the expected-signal tables assume the rectilinear lattice and one field
|
|
1080
|
+
factor), so they have nothing to add here.
|
|
1081
|
+
- **Sampling** is the lattice sampling below, unchanged. Measured against a fine-grid oracle: the environment PSF
|
|
1082
|
+
quadrature with the disc sampled 5 times finer than the site pitch (3 times with the box aperture), on a 16 × 12
|
|
1083
|
+
slanted edge with the 5.95-site disc (50 mm f/4, subject 2 m, focus 1.8 m, 6 µm, 80 nm spectral step), as the relative
|
|
1084
|
+
difference in expected electrons per site (max / RMS). Each image-space case stays within the rectilinear capture's
|
|
1085
|
+
error (the motion-blur rows are lower because the rotation smears the edge):
|
|
1086
|
+
|
|
1087
|
+
| Case (#548) | Point aperture | 4 × 4 box |
|
|
1088
|
+
| --- | --- | --- |
|
|
1089
|
+
| rectilinear, no option (object-space path) | 0.110 / 0.026 | 0.032 / 0.0060 |
|
|
1090
|
+
| `lens.vignetting` (r2 −0.3 at 0.2 mm) | 0.110 / 0.026 | 0.032 / 0.0061 |
|
|
1091
|
+
| `"fisheye-equisolid-angle"` | 0.110 / 0.026 | 0.032 / 0.0060 |
|
|
1092
|
+
| radial distortion, k1 −0.2 at 0.25 mm | 0.110 / 0.026 | 0.032 / 0.0060 |
|
|
1093
|
+
| rotation pitch 0.2, yaw 0.5, roll 3 rad/s, 2 midpoints | 0.069 / 0.016 | 0.017 / 0.0029 |
|
|
1094
|
+
| all of them (equidistant fisheye, Brown–Conrady) | 0.065 / 0.015 | 0.015 / 0.0027 |
|
|
1095
|
+
|
|
1096
|
+
The recorded bounds on these frames were small: distortion local scale 0.046 (radial) and 0.044 (Brown–Conrady),
|
|
1097
|
+
rotation 5.0e-4 sites, projection 4.7e-5 sites (a frame 0.1 mm wide sees little field angle). On a real frame the
|
|
1098
|
+
projection and distortion bounds grow with the field: read them from the assumption. On the app's full frame
|
|
1099
|
+
(36 × 24 mm at 93.75 µm, 50 mm f/2.8, subject 2 m, focus 1 m, a 5.0-site disc): an equisolid fisheye 0.43 sites
|
|
1100
|
+
(8.6 % of the disc), radial distortion k1 −0.1 at 25 mm a local scale 0.28 off 1 at the corners, yaw 0.05 and pitch
|
|
1101
|
+
0.02 rad/s at 1/125 s 8e-4 sites.
|
|
1102
|
+
- **Cost** at the app's defaults (Speed preset: 384 × 256 whole frame at 93.75 µm with the 2 × 2 box; Siemens star;
|
|
1103
|
+
50 mm f/2.8, subject 2 m; Apple M5 Pro, Node.js 24.19.0, built `dist`, median [range] of 3, one round for the last
|
|
1104
|
+
row; a shared machine, so the slow rows varied by up to a third between sessions). The image-space convolution itself
|
|
1105
|
+
is `diagnostics.timingsMs.defocus`; with one kernel for every node (no diffraction) it convolves once per scene
|
|
1106
|
+
material in each band instead of once per spectral node, the same sum regrouped.
|
|
1107
|
+
|
|
1108
|
+
| Option | In focus | Focus 1 m (R 3) | Focus 0.6 m (R 6) |
|
|
1109
|
+
| --- | --- | --- | --- |
|
|
1110
|
+
| none (object-space path) | 0.06 s | 0.05 s | 0.07 s |
|
|
1111
|
+
| `lens.vignetting` | 1.0 s | 1.6 s [1.5–1.6] (convolution 0.27 s) | 1.7 s (0.38 s) |
|
|
1112
|
+
| `"fisheye-equisolid-angle"` | 11 s | 15–19 s (0.18 s) | |
|
|
1113
|
+
| radial distortion | 18–21 s | 19–20 s (0.36 s) | |
|
|
1114
|
+
| rotation (yaw 0.05, pitch 0.02 rad/s, 8 midpoints) | 2.1–3.3 s | 7.5–8.8 s (1.3–2.2 s) | 6.5 s (1.8 s) |
|
|
1115
|
+
| fisheye + distortion + vignetting + rotation | 94–97 s | 101–124 s (3.4–5.9 s) | |
|
|
1116
|
+
|
|
1117
|
+
The in-focus cost of a lens mapping is the environment's per-site query path, which `lens.defocus` does not change;
|
|
1118
|
+
out of focus adds the band convolutions and one lattice lookup per query (a forward map of the query's ray to the
|
|
1119
|
+
sensor). Without a lens mapping the lean executor reads the values by lattice key, as for the diffraction provider.
|
|
1120
|
+
In focus every request is byte-identical to the same request without the option.
|
|
959
1121
|
- **Sampling.** The scene enters at the lattice points the sites query: site centres, or the pixel-aperture nodes
|
|
960
1122
|
and anti-aliasing spots. So near an edge the result can differ from the continuous disc integral by the lattice's
|
|
961
1123
|
point sampling of the edge. Measured on a 16 × 12 slanted edge, as the relative difference in expected electrons
|
|
@@ -1549,10 +1711,12 @@ Validated against real full-resolution captures resized in linear light: three s
|
|
|
1549
1711
|
|
|
1550
1712
|
**Cost.** Where a display pixel covers more than 25 real pixels, the cost follows the detailed display pixels ×
|
|
1551
1713
|
`samplesPerAxis`², not the real pixel count; at 25 or fewer (the exact area mean) it follows the real pixels the region
|
|
1552
|
-
touches. A device-pixel view of a 24 MP frame, 2028 × 1352 in 96 × 96 parts from one prepared view, takes about 0.
|
|
1553
|
-
per part with the engine defaults and about 0.
|
|
1554
|
-
|
|
1555
|
-
|
|
1714
|
+
touches. A device-pixel view of a 24 MP frame, 2028 × 1352 in 96 × 96 parts from one prepared view, takes about 0.04 s
|
|
1715
|
+
per part with the engine defaults and about 0.08 s with the Quality options. Those are the medians on 2.15.0: Node 24.19.0,
|
|
1716
|
+
one thread on an Apple M5 Pro under a load average of about 6, two repeats. On 2.9.0 the same parts took about 0.09 s and
|
|
1717
|
+
0.18 s on a 4-vCPU 2.80 GHz Xeon (Node 24.21.0). Both measurements are in
|
|
1718
|
+
[the tables](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#a-device-pixel-view-rendered-in-parts), from
|
|
1719
|
+
`node scripts/benchmark-display-view.mjs --app`. The table below is the 2.6.0 record of whole views against the
|
|
1556
1720
|
whole-frame view of the same request (Siemens star, f/8, ISO 100, one run, same host; every row, and the times before
|
|
1557
1721
|
the exact speed-ups, in [DISPLAY_VIEW.md](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#cost)):
|
|
1558
1722
|
|
|
@@ -1605,9 +1769,23 @@ what ran (see [Rendering on the GPU](https://github.com/Photivra/photivra/blob/m
|
|
|
1605
1769
|
`signal` applies to preparing; give each render its own. Keep one prepared view per request in each worker: measured on
|
|
1606
1770
|
a 24 MP request, an 8 × 8 region took 6–40 ms through a prepared view instead of 0.24–0.41 s as its own call (Speed).
|
|
1607
1771
|
|
|
1772
|
+
**Highlight count (#533).** `diagnostics.highlight.saturatedPixelCount` is the number of the real capture's output pixels
|
|
1773
|
+
in the rendered region that are saturated (a clipped contributing RAW site), as `simulateCapture` reports it. It is the
|
|
1774
|
+
expected value over the noise (`statistic: "expected-value"`), because the capture's own count is one noise sample:
|
|
1775
|
+
|
|
1776
|
+
- it equals the capture's count where every site is far from white;
|
|
1777
|
+
- it is a real number, not an integer;
|
|
1778
|
+
- the counts of regions that tile a view add up to the view's count;
|
|
1779
|
+
- it changes no sample;
|
|
1780
|
+
- with AHD it counts the sites the view's AHD candidates read, so near a clipped corner it can be lower than the capture's
|
|
1781
|
+
count.
|
|
1782
|
+
|
|
1783
|
+
See [Highlight count](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#highlight-count-533).
|
|
1784
|
+
|
|
1608
1785
|
**Not supported** (refused with an `InvalidConfigurationError`): `lens.pupilSampling`, `lens.projection`,
|
|
1609
1786
|
`lens.distortion`, `lens.vignetting`, `lens.psf`, camera rotation, `output.whiteBalance: "auto"`, the luminance tone
|
|
1610
|
-
curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `
|
|
1787
|
+
curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `output.exposureCompensationStops` is
|
|
1788
|
+
supported (#556). `lens.defocus` is rendered as a
|
|
1611
1789
|
site kernel in the expected signal (#492; see [Optics](#optics)). `onProgress` is not called; `signal` cancels.
|
|
1612
1790
|
|
|
1613
1791
|
The result is labelled `kind: "display-view"` with a `label` for the user interface; it has no RAW, plan or file, and
|
package/docs/USAGE.md
CHANGED
|
@@ -3293,6 +3293,9 @@ void develop;
|
|
|
3293
3293
|
|
|
3294
3294
|
- `highlightMode: "reconstruct"` blends pixels above 0.8 of white towards neutral, instead of clipping
|
|
3295
3295
|
each WB-scaled channel. It is an approximation, not a recovery of the clipped signal.
|
|
3296
|
+
- `exposureCompensationStops` (#556, −2 to +5, default 0) multiplies each site's black-subtracted, white-normalized
|
|
3297
|
+
value by 2^stops before demosaic and white balance. A site pushed to white clips and counts as saturated; the RAW
|
|
3298
|
+
codes are not changed. 0 develops byte for byte as before. The WebGPU accelerator rejects a non-zero push.
|
|
3296
3299
|
- `extended-reinhard-luminance` takes `{ whiteLuminance }`, the luminance mapped to output white.
|
|
3297
3300
|
- `createPhotographicExportPair(input, { signal })` accepts an optional `AbortSignal`.
|
|
3298
3301
|
|
|
@@ -7802,16 +7805,24 @@ import { simulateDisplayView } from "@photivra/engine";
|
|
|
7802
7805
|
const view = await simulateDisplayView({ scene: { preset: "slanted-edge" }, sensor: { pixelPitchMicrometers: 3 }, output: { width: 600, height: 400 } },
|
|
7803
7806
|
{ width: 60, height: 40 });
|
|
7804
7807
|
console.log(view.kind, view.view.scaleX, view.assumptions.find((a) => a.id === "view.definition")?.value);
|
|
7808
|
+
// #533: the real capture's clipped-highlight pixels in the rendered region, as the expected value of simulateCapture's count.
|
|
7809
|
+
console.log(view.diagnostics.highlight.saturatedPixelCount, view.diagnostics.highlight.statistic); // a real number, "expected-value"
|
|
7805
7810
|
```
|
|
7806
7811
|
|
|
7812
|
+
`diagnostics.highlight.saturatedPixelCount` is the expected number of the real capture's output pixels in the rendered
|
|
7813
|
+
region that have a clipped contributing site. `simulateCapture` counts the same pixels on its one noise sample, so the two
|
|
7814
|
+
are equal where every site is far from white. Counts of regions that tile a view add up to the whole view's count. See
|
|
7815
|
+
[Highlight count](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#highlight-count-533).
|
|
7816
|
+
|
|
7807
7817
|
#### Rendering a prepared view on the GPU (2.11, #481)
|
|
7808
7818
|
|
|
7809
7819
|
`prepareDisplayView(request, { gpu })` chooses the renderer: `"off"` (the default) is the CPU reference, `"auto"` the GPU
|
|
7810
7820
|
when a conforming device is available and the request is supported (else the CPU, labelled), `"require"` fails instead of
|
|
7811
7821
|
falling back. `gpuDevice` hands the view a worker's `navigator.gpu` or an `ImageKernelAccelerator` whose device it shares.
|
|
7812
|
-
Every render says which path ran in `diagnostics.fast.execution`, in the accelerator's words.
|
|
7813
|
-
|
|
7814
|
-
|
|
7822
|
+
Every render says which path ran in `diagnostics.fast.execution`, in the accelerator's words. The engine's renderer runs the
|
|
7823
|
+
Speed (#478) and Quality (#479) kernels on a conforming device for the requests it covers. Elsewhere `"auto"` runs the CPU
|
|
7824
|
+
reference and gives the reason (in Node without a WebGPU entry point, for example); see
|
|
7825
|
+
[Rendering on the GPU](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#rendering-on-the-gpu).
|
|
7815
7826
|
|
|
7816
7827
|
```ts
|
|
7817
7828
|
import { prepareDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
|
|
@@ -31,6 +31,14 @@ export function createNativeRawDevelopmentTask(input: NativeRawDevelopmentInput,
|
|
|
31
31
|
|
|
32
32
|
This call is synchronous; any supplied provider must follow its explicit synchronous contract.
|
|
33
33
|
|
|
34
|
+
## NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS
|
|
35
|
+
|
|
36
|
+
Accepted range of `NativeRawDevelopmentInput.exposureCompensationStops` and `simulateCapture`'s `output.exposureCompensationStops` (#556).
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS = Object.freeze({ minimum: -2, maximum: 5 } as const)
|
|
40
|
+
```
|
|
41
|
+
|
|
34
42
|
## NativeRawDevelopmentInput
|
|
35
43
|
|
|
36
44
|
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
@@ -56,6 +64,14 @@ export interface NativeRawDevelopmentInput {
|
|
|
56
64
|
* always true for a 2×2 Bayer pattern. Interior pixels are unchanged.
|
|
57
65
|
*/
|
|
58
66
|
borderHandling?: RawReconstructionBorderHandling;
|
|
67
|
+
/**
|
|
68
|
+
* Development exposure push in stops (#556), from {@link NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS} −2 to +5; default 0.
|
|
69
|
+
* Each site's black-subtracted, white-normalized value is multiplied by 2^stops before demosaic, white balance, the
|
|
70
|
+
* highlight mode, colour and the tone curve, like a raw editor's exposure slider. A site whose pushed value reaches the
|
|
71
|
+
* white level is clipped there and counted as saturated (so `highlightMode: "reconstruct"` rolls it off); values below
|
|
72
|
+
* black stay signed. The RAW codes are not changed. 0 (or omitted) develops exactly as before, byte for byte.
|
|
73
|
+
*/
|
|
74
|
+
exposureCompensationStops?: number;
|
|
59
75
|
/** Counts the kept packed RAW plus the integer output. It does not count caller buffers, process heap or GPU memory. */
|
|
60
76
|
maximumRetainedPayloadBytes: number;
|
|
61
77
|
}
|
package/docs/api/core-version.md
CHANGED
|
@@ -11,5 +11,5 @@ version they were made with. Proof-of-concept (POC), schema and model contracts
|
|
|
11
11
|
own versions. Release checks confirm it matches package.json and the packed package.
|
|
12
12
|
|
|
13
13
|
```ts
|
|
14
|
-
ENGINE_API_VERSION = "2.
|
|
14
|
+
ENGINE_API_VERSION = "2.16.0" as const
|
|
15
15
|
```
|
package/docs/api/exports.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"packageVersion": "2.
|
|
3
|
-
"engineApiVersion": "2.
|
|
2
|
+
"packageVersion": "2.16.0",
|
|
3
|
+
"engineApiVersion": "2.16.0",
|
|
4
4
|
"exports": [
|
|
5
5
|
{
|
|
6
6
|
"name": "AbsoluteUncertaintyEstimate",
|
|
@@ -2802,6 +2802,11 @@
|
|
|
2802
2802
|
"source": "src/facade/display-view-gpu.ts",
|
|
2803
2803
|
"kind": "type"
|
|
2804
2804
|
},
|
|
2805
|
+
{
|
|
2806
|
+
"name": "DisplayViewHighlightDiagnostics",
|
|
2807
|
+
"source": "src/facade/simulate-capture.ts",
|
|
2808
|
+
"kind": "type"
|
|
2809
|
+
},
|
|
2805
2810
|
{
|
|
2806
2811
|
"name": "DisplayViewResult",
|
|
2807
2812
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -4577,6 +4582,11 @@
|
|
|
4577
4582
|
"source": "src/capture/native-environment-optical-stage.ts",
|
|
4578
4583
|
"kind": "runtime"
|
|
4579
4584
|
},
|
|
4585
|
+
{
|
|
4586
|
+
"name": "NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS",
|
|
4587
|
+
"source": "src/capture/native-raw-development.ts",
|
|
4588
|
+
"kind": "runtime"
|
|
4589
|
+
},
|
|
4580
4590
|
{
|
|
4581
4591
|
"name": "NATIVE_RAW_DNG_SCHEMA_VERSION",
|
|
4582
4592
|
"source": "src/capture/native-raw-dng.ts",
|
|
@@ -8347,6 +8357,16 @@
|
|
|
8347
8357
|
"source": "src/facade/simulate-capture.ts",
|
|
8348
8358
|
"kind": "type"
|
|
8349
8359
|
},
|
|
8360
|
+
{
|
|
8361
|
+
"name": "SimulateCaptureDetailDiagnostics",
|
|
8362
|
+
"source": "src/facade/simulate-capture.ts",
|
|
8363
|
+
"kind": "type"
|
|
8364
|
+
},
|
|
8365
|
+
{
|
|
8366
|
+
"name": "SimulateCaptureDiffractionCorrection",
|
|
8367
|
+
"source": "src/facade/simulate-capture.ts",
|
|
8368
|
+
"kind": "type"
|
|
8369
|
+
},
|
|
8350
8370
|
{
|
|
8351
8371
|
"name": "SimulateCaptureDngPart",
|
|
8352
8372
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -8412,6 +8432,11 @@
|
|
|
8412
8432
|
"source": "src/facade/simulate-capture.ts",
|
|
8413
8433
|
"kind": "type"
|
|
8414
8434
|
},
|
|
8435
|
+
{
|
|
8436
|
+
"name": "SimulateCaptureSharpening",
|
|
8437
|
+
"source": "src/facade/simulate-capture.ts",
|
|
8438
|
+
"kind": "type"
|
|
8439
|
+
},
|
|
8415
8440
|
{
|
|
8416
8441
|
"name": "SimulateCaptureWhiteBalance",
|
|
8417
8442
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
The package and root API versions are recorded once in [Navigation](../API_REFERENCE.md) · [Developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md). Generated signatures retain independent schema/model versions. Only the exports listed here are root-package contracts; module-local helpers are not supported deep imports.
|
|
4
4
|
|
|
5
|
+
## DisplayViewHighlightDiagnostics
|
|
6
|
+
|
|
7
|
+
#533: the display view's highlight count. `simulateCapture` reports `diagnostics.highlight.saturatedPixelCount`, the
|
|
8
|
+
output pixels with at least one clipped contributing RAW site (a site flagged saturated or at the white level) counted on its
|
|
9
|
+
one noise sample (`sensor.noiseSeed`). The display view renders no RAW, so it reports that count's expected value over the
|
|
10
|
+
noise: Σ_j w_j·P_j over the real output pixels j of the rendered region, w_j the part of pixel j inside the region (1 inside,
|
|
11
|
+
a fraction at a region edge that cuts a real pixel) and P_j the probability, under the view's own site model (each site's
|
|
12
|
+
normalized value normal with its exact expected electrons, shot, dark, read and ADC noise, as the expected value uses), that at
|
|
13
|
+
least one site pixel j's demosaic reads is at or above the white level, the sites' noises independent. It is estimated as the
|
|
14
|
+
view's expected value is (`view.estimation`: exact over every real pixel, or the stratified samples), so it is not an integer.
|
|
15
|
+
|
|
16
|
+
- Where every contributing site is far from white (more than 8.5 noise standard deviations either side) P_j is exactly 0 or 1,
|
|
17
|
+
and the expected count equals the capture's count of the same pixels exactly (they then have no noise to differ by).
|
|
18
|
+
- Regions are additive: the counts of regions that tile the view sum to the whole view's count (up to floating-point summation
|
|
19
|
+
order), each display pixel's share being the same in every region that renders it.
|
|
20
|
+
- With the bilinear demosaic the sites are the capture's own contributing sites. With AHD the capture counts every site within
|
|
21
|
+
3 sites of the pixel (its interpolation reach, clipped to the raster); the view counts the 25–29 of those 49 that its AHD
|
|
22
|
+
candidates read, so its count can be lower where a clipped area ends in a corner or a speck 2–3 sites away diagonally
|
|
23
|
+
(an approximation; along a straight clipped edge the two agree).
|
|
24
|
+
- It never changes a sample of the view. On the GPU (`prepareDisplayView` with `gpu`) it is the CPU reference's value: the
|
|
25
|
+
kernel decides P_j only where it is certainly 0 or 1 and the host computes every other pixel's share with the reference.
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
export interface DisplayViewHighlightDiagnostics {
|
|
29
|
+
/** Expected number of saturated real output pixels in the rendered region (a non-negative real number; see above). */
|
|
30
|
+
saturatedPixelCount: number;
|
|
31
|
+
/** The statistic: always "expected-value" (the capture's own count is one noise sample of it). */
|
|
32
|
+
statistic: "expected-value";
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
5
36
|
## DisplayViewResult
|
|
6
37
|
|
|
7
38
|
Result of {@link simulateDisplayView }: the full-resolution capture resized for display, estimated; not a capture.
|
|
@@ -43,7 +74,9 @@ export interface DisplayViewResult {
|
|
|
43
74
|
diagnostics: { sampleCount: number; siteEvaluations: number; sceneLookups: number; detailPixelCount: number; uniformPixelCount: number; ahdCorrectedPixelCount: number;
|
|
44
75
|
timingsMs: { view: number; total: number };
|
|
45
76
|
/** #395: exact or fast, as requested and as run; 2.11 (#481): plus `execution`, where the render ran (see {@link DisplayViewFastReport}). */
|
|
46
|
-
fast: DisplayViewFastReport
|
|
77
|
+
fast: DisplayViewFastReport;
|
|
78
|
+
/** #533: the saturated (clipped highlight) pixels of the real capture in the rendered region, as an expected value (see {@link DisplayViewHighlightDiagnostics}). */
|
|
79
|
+
highlight: DisplayViewHighlightDiagnostics };
|
|
47
80
|
provenance: { scientificStatus: "approximation"; cameraPresetId: string; cameraPresetVersion: string; evidence: readonly EvidenceProvenance[];
|
|
48
81
|
composedContracts: readonly string[]; limitations: readonly string[] };
|
|
49
82
|
/** The capture request's assumptions, then `view.definition`, `view.estimation`, `view.noise`, `view.demosaic` and `view.source`. */
|
|
@@ -111,7 +144,7 @@ a capture does. `motion.rotation` is a non-zero angular velocity; `sensor.tempor
|
|
|
111
144
|
SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling", "lens.projection", "lens.distortion", "lens.vignetting", "lens.psf",
|
|
112
145
|
"lens.diffraction", "lens.defocus", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
113
146
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
114
|
-
"output.format.jpeg", "output.format.png", "output.format.dng"] as const)
|
|
147
|
+
"output.diffractionCorrection", "output.sharpening", "output.format.jpeg", "output.format.png", "output.format.dng"] as const)
|
|
115
148
|
```
|
|
116
149
|
|
|
117
150
|
## SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX
|
|
@@ -334,6 +367,56 @@ export type SimulateCaptureDemosaic = "bilinear" | "ahd";
|
|
|
334
367
|
export interface SimulateCaptureDemosaicRecord { kind: SimulateCaptureDemosaic; fast?: boolean }
|
|
335
368
|
```
|
|
336
369
|
|
|
370
|
+
## SimulateCaptureDetailDiagnostics
|
|
371
|
+
|
|
372
|
+
`diagnostics.detail` (#552): what the development detail stages did. Present only when one ran.
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
export interface SimulateCaptureDetailDiagnostics {
|
|
376
|
+
diffractionCorrection?: {
|
|
377
|
+
/** Corrected linear samples below 0 (ringing below black; clipped later by gamut handling) and above the RAW white level 1. */
|
|
378
|
+
negativeSampleCount: number;
|
|
379
|
+
aboveWhiteSampleCount: number;
|
|
380
|
+
/** The diffraction cut-off p/(λ·N_w) in cycles per pixel (Nyquist is 0.5); nothing beyond it is recovered. */
|
|
381
|
+
cutoffCyclesPerPixel: number;
|
|
382
|
+
/** Measured largest |response| of the filter, and largest |response| beyond the cut-off (null when the Nyquist square is inside it). */
|
|
383
|
+
responsePeakGain: number;
|
|
384
|
+
responseMaximumBeyondCutoff: number | null;
|
|
385
|
+
};
|
|
386
|
+
sharpening?: {
|
|
387
|
+
/** Half-width ⌈3σ⌉ of the Gaussian in pixels. */
|
|
388
|
+
kernelRadiusPixels: number;
|
|
389
|
+
/** Samples changed by the mask, and samples left alone because their detail was under the threshold. */
|
|
390
|
+
sharpenedSampleCount: number;
|
|
391
|
+
thresholdHeldSampleCount: number;
|
|
392
|
+
/** Sharpened samples clipped at 0 or 1. */
|
|
393
|
+
clippedLowSampleCount: number;
|
|
394
|
+
clippedHighSampleCount: number;
|
|
395
|
+
/** Largest increase and decrease amount·detail before clipping, in encoded units: the halo strength. */
|
|
396
|
+
maximumIncrease: number;
|
|
397
|
+
maximumDecrease: number;
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## SimulateCaptureDiffractionCorrection
|
|
403
|
+
|
|
404
|
+
`output.diffractionCorrection` (#552): the Wiener–Tikhonov gain G(s) = min(maximumGain, H(s)(1 + α)/(H(s)² + α)) on the circular-pupil
|
|
405
|
+
OTF H at s = f/f_c, f_c = 1/(λ·N_w), as a (2R + 1)² FIR per channel. Every field is optional.
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
export interface SimulateCaptureDiffractionCorrection {
|
|
409
|
+
/** Design wavelength in nm, 380–780, used for all three channels. Default 550. */
|
|
410
|
+
wavelengthNm?: number;
|
|
411
|
+
/** α, an assumed noise-to-signal power ratio, 1e-4–1. Larger is safer and restores less. Default 0.01. */
|
|
412
|
+
regularization?: number;
|
|
413
|
+
/** Upper bound on the gain at any frequency, 1–10 (it bounds the noise amplification). Default 4. */
|
|
414
|
+
maximumGain?: number;
|
|
415
|
+
/** FIR radius R in pixels, 1–16. Default: the Airy pattern's third dark ring at the design wavelength, within 1–16. */
|
|
416
|
+
kernelRadiusPixels?: number;
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
337
420
|
## SimulateCaptureDngPart
|
|
338
421
|
|
|
339
422
|
One part of a whole-frame DNG (#391): a region render's packed RAW and where its native sites lie (`result.raw`, `result.region`).
|
|
@@ -544,6 +627,16 @@ export interface SimulateCaptureOptions {
|
|
|
544
627
|
toneCurveParameters?: SdrToneCurveParameters;
|
|
545
628
|
/** Default "clip". */
|
|
546
629
|
highlightMode?: RawHighlightMode;
|
|
630
|
+
/**
|
|
631
|
+
* Opt-in (#556) development exposure push in stops, −2 to +5 (`NATIVE_RAW_DEVELOPMENT_EXPOSURE_RANGE_STOPS`); default 0.
|
|
632
|
+
* A linear gain 2^stops on the RAW after black subtraction and white normalization, before demosaic, white balance,
|
|
633
|
+
* the highlight mode, colour, the tone curve and encoding, like a raw editor's exposure slider ("brighten in development").
|
|
634
|
+
* A site pushed to or past white clips there and counts as saturated (`highlightMode: "reconstruct"` rolls it off);
|
|
635
|
+
* raised shadows keep their real noise and ADC quantization. The RAW codes, `result.raw` and a DNG are not changed;
|
|
636
|
+
* the developed samples (`image.samples`, also with format "dng"), JPEG, PNG, the ideal reference and the display view
|
|
637
|
+
* honour it. Reported in the assumption `output.exposureCompensationStops`. 0 or omitted: byte-identical to before.
|
|
638
|
+
*/
|
|
639
|
+
exposureCompensationStops?: number;
|
|
547
640
|
/** Default "scene-illuminant". */
|
|
548
641
|
whiteBalance?: SimulateCaptureWhiteBalance;
|
|
549
642
|
/** JPEG constant quantization step 1-255 (not a manufacturer 'quality'). Default 2. */
|
|
@@ -587,6 +680,20 @@ export interface SimulateCaptureOptions {
|
|
|
587
680
|
* as a capture of the region's size.
|
|
588
681
|
*/
|
|
589
682
|
region?: { x: number; y: number; width: number; height: number };
|
|
683
|
+
/**
|
|
684
|
+
* Opt-in (#552) diffraction correction: a bounded, regularized deconvolution of the developed linear colour with the
|
|
685
|
+
* diffraction OTF of the ideal circular pupil at the capture's working f-number (see {@link SimulateCaptureDiffractionCorrection}).
|
|
686
|
+
* It runs after demosaic, colour and white balance and before the tone curve. Frequencies beyond the diffraction cut-off
|
|
687
|
+
* 1/(λ·N_w) never reached the sensor and stay lost: the correction's design gain there is 0. Needs `lens.diffraction`.
|
|
688
|
+
* Not combinable with `region`. The DNG's RAW codes are not changed. Default off.
|
|
689
|
+
*/
|
|
690
|
+
diffractionCorrection?: SimulateCaptureDiffractionCorrection;
|
|
691
|
+
/**
|
|
692
|
+
* Opt-in (#552) unsharp mask (see {@link SimulateCaptureSharpening}) on the sRGB-encoded values after the tone curve and gamut
|
|
693
|
+
* handling, before quantization. It raises edge contrast and creates halos; it adds no optical detail. sRGB output only
|
|
694
|
+
* (not the wide-gamut or perceptual PNG paths); not combinable with `region`. The DNG's RAW codes are not changed. Default off.
|
|
695
|
+
*/
|
|
696
|
+
sharpening?: SimulateCaptureSharpening;
|
|
590
697
|
};
|
|
591
698
|
/** Camera motion and shutter type. Default: still camera, global electronic shutter. */
|
|
592
699
|
motion?: SimulateCaptureMotion;
|
|
@@ -708,7 +815,7 @@ export interface SimulateCaptureResult {
|
|
|
708
815
|
* scene provider gives the same expected signal (to float rounding) through the per-tap environment quadrature.
|
|
709
816
|
*/
|
|
710
817
|
diffraction?: { profile: DiffractionLimitedPsfProfile; psf: Extract<CalculateEnvironmentSensorPhotoSignalInput["psf"], { kind: "sampled-local" }>;
|
|
711
|
-
execution: "object-space-convolution" | "environment-psf-quadrature" };
|
|
818
|
+
execution: "object-space-convolution" | "environment-psf-quadrature" | "image-space-convolution" };
|
|
712
819
|
};
|
|
713
820
|
exposure: {
|
|
714
821
|
mode: SimulateCaptureExposureMode;
|
|
@@ -781,6 +888,8 @@ export interface SimulateCaptureResult {
|
|
|
781
888
|
rendering: SdrRenderingResult["diagnostics"];
|
|
782
889
|
/** Present with output.gamutMapping "perceptual": pixels whose chroma or lightness the mapping changed. */
|
|
783
890
|
gamutMapping?: { method: "cielab-hue-preserving-chroma-compression"; kneeFraction: number; adjustedPixelCount: number; lightnessClippedPixelCount: number };
|
|
891
|
+
/** #552: present with output.diffractionCorrection or output.sharpening. */
|
|
892
|
+
detail?: SimulateCaptureDetailDiagnostics;
|
|
784
893
|
/**
|
|
785
894
|
* Wall-clock milliseconds per stage. `raw`, `whiteBalance` (the second RAW pass of auto white balance), `development`
|
|
786
895
|
* (includes DNG writing), `jpeg` and `total` are whole milliseconds (`Date.now()`). Since 2.1, where a stage ran:
|
|
@@ -831,6 +940,22 @@ export type SimulateCaptureSensorAntiAliasing =
|
|
|
831
940
|
| { kind: "point-splitting"; components: readonly AntiAliasingPointSplitComponent[]; fast?: boolean };
|
|
832
941
|
```
|
|
833
942
|
|
|
943
|
+
## SimulateCaptureSharpening
|
|
944
|
+
|
|
945
|
+
`output.sharpening` (#552): unsharp mask y = x + amount·(x − G_σ∗x) where |x − G_σ∗x| ≥ threshold (GIMP and ImageMagick
|
|
946
|
+
unsharp mask), per channel on sRGB-encoded values in [0, 1], then clipped to [0, 1]. Every field is optional.
|
|
947
|
+
|
|
948
|
+
```ts
|
|
949
|
+
export interface SimulateCaptureSharpening {
|
|
950
|
+
/** Gaussian standard deviation σ in output pixels, (0, 8]; the kernel reaches ⌈3σ⌉. Default 1. */
|
|
951
|
+
radiusPixels?: number;
|
|
952
|
+
/** Gain on the detail signal, (0, 5]. Default 0.5. */
|
|
953
|
+
amount?: number;
|
|
954
|
+
/** Detail smaller than this (encoded units, [0, 1]) is left unchanged. Default 0 (every difference is sharpened). */
|
|
955
|
+
threshold?: number;
|
|
956
|
+
}
|
|
957
|
+
```
|
|
958
|
+
|
|
834
959
|
## SimulateCaptureWhiteBalance
|
|
835
960
|
|
|
836
961
|
The exact contract is declared below; use the domain guide for assumptions and staged integration.
|
|
@@ -14,8 +14,9 @@ so for the same focal length, f-number, distances and pitch a request they refus
|
|
|
14
14
|
`withinLimit: false`, and one they accept has `withinLimit: true`. Pass the values the capture uses: in an automatic
|
|
15
15
|
exposure mode that is the resolved f-number. The limit applies to the disc alone; with `lens.diffraction` the combined
|
|
16
16
|
kernel (diffraction radius plus defocus radius, at most 32 + 64 sites) has no further limit. Other reasons a capture
|
|
17
|
-
refuses `lens.defocus` out of focus (
|
|
18
|
-
|
|
17
|
+
refuses `lens.defocus` out of focus (pupil sampling, a caller PSF, camera rotation with a rolling shutter, a vignetting or
|
|
18
|
+
distortion profile that does not reach the kernel's radius beyond the frame, and in the display view a projection,
|
|
19
|
+
distortion, vignetting or rotation) are not checked here.
|
|
19
20
|
|
|
20
21
|
The blur circle is a geometric thin-lens approximation for an ideal uniform circular pupil (no diffraction, aberrations
|
|
21
22
|
or pupil vignetting). Inputs follow `calculateDefocusCircle`: all positive and finite, both distances beyond the focal
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photivra/engine",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.16.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Scientific TypeScript engine for camera optics, exposure, focus, motion, sensor sampling, diffraction, and digital imaging calculations.",
|
|
6
6
|
"type": "module",
|