@photivra/engine 2.8.1 → 2.10.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 +144 -1
- package/CITATION.cff +1 -1
- package/README.md +8 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +2 -0
- package/dist/capture/export-jpeg.d.ts +19 -0
- package/dist/capture/export-jpeg.js +190 -108
- package/dist/capture/export-jpeg.js.map +1 -1
- package/dist/capture/native-environment-lean.js +106 -12
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.d.ts +37 -0
- package/dist/capture/native-environment-tabulated.js +42 -6
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/core/portable-math.d.ts +13 -0
- package/dist/core/portable-math.js +43 -0
- package/dist/core/portable-math.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/channel-response.d.ts +15 -0
- package/dist/facade/channel-response.js +33 -0
- package/dist/facade/channel-response.js.map +1 -0
- package/dist/facade/diffraction-provider.js +50 -15
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +75 -0
- package/dist/facade/display-view.js +471 -109
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.js +2 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +36 -4
- package/dist/facade/expected-signal.js +331 -43
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/scene-radiance.d.ts +4 -1
- package/dist/facade/scene-radiance.js +4 -4
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +77 -5
- package/dist/facade/simulate-capture.js +232 -40
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +25 -2
- package/dist/facade/simulate-display-view.js +55 -21
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/image-kernel-execution.d.ts +5 -0
- package/dist/gpu/image-kernel-execution.js +2 -2
- package/dist/gpu/image-kernel-execution.js.map +1 -1
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +19 -17
- package/dist/optics/diffraction-psf-profile.js +49 -18
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/irradiance-convolution.d.ts +11 -0
- package/dist/optics/irradiance-convolution.js +64 -24
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +13 -0
- package/dist/optics/scene-to-sensor-irradiance.js +58 -24
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/optics/sensor-aperture-rays.js +2 -1
- package/dist/optics/sensor-aperture-rays.js.map +1 -1
- package/dist/optics/sensor-environment-query.js +4 -3
- package/dist/optics/sensor-environment-query.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +16 -0
- package/dist/optics/site-diffraction-kernel.js +60 -8
- package/dist/optics/site-diffraction-kernel.js.map +1 -1
- package/dist/output/print-jpeg.d.ts +28 -1
- package/dist/output/print-jpeg.js +79 -7
- package/dist/output/print-jpeg.js.map +1 -1
- package/dist/processing/demosaic.js +114 -37
- package/dist/processing/demosaic.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +8 -0
- package/dist/schema/scene-radiance.js +28 -17
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/environment-photo-signal.d.ts +21 -0
- package/dist/sensor/environment-photo-signal.js +68 -2
- package/dist/sensor/environment-photo-signal.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +12 -0
- package/dist/sensor/stochastic-seed.js +25 -0
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/API_REFERENCE.md +23 -1
- package/docs/QUICK_START.md +77 -8
- package/docs/USAGE.md +30 -0
- package/docs/api/capture-export-jpeg.md +10 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +77 -2
- package/docs/api/facade-display-view.md +50 -0
- package/docs/api/facade-scene-radiance.md +2 -1
- package/docs/api/facade-simulate-capture.md +88 -8
- package/docs/api/facade-simulate-display-view.md +23 -1
- package/docs/api/gpu-image-kernel-execution.md +5 -0
- package/docs/api/optics-diffraction-psf-profile.md +1 -1
- package/docs/api/output-print-jpeg.md +41 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
2
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
3
|
+
import { hypot3 } from "../core/portable-math.js";
|
|
3
4
|
import { apertureRadianceQuery } from "../sensor/environment-photo-signal.js";
|
|
4
5
|
import { createSensorEqeTemporalPhotoSignal } from "../sensor/temporal-photo-signal.js";
|
|
5
6
|
import { isDeeplyFrozen } from "../core/internal-radiance-evaluator.js";
|
|
@@ -21,7 +22,7 @@ import { composeNativeEnvironmentSensorSite } from "./native-environment-optical
|
|
|
21
22
|
import { calculateNativeRawSite, parseNativeRawProducerSite } from "./sensor-raw-producer.js";
|
|
22
23
|
import { calculateSiteEffectiveSaturationCode } from "./raw-white-level.js";
|
|
23
24
|
import { adcTransfer, resolveSensorReadoutRegime, samplePoisson } from "../sensor/raw-readout.js";
|
|
24
|
-
import {
|
|
25
|
+
import { createResettableUniformStream, deriveNativeSiteStreamSeedUint32, sampleDeterministicStandardNormal, NATIVE_SITE_CHARGE_STREAM_ID, NATIVE_SITE_READ_NOISE_STREAM_ID } from "../sensor/stochastic-seed.js";
|
|
25
26
|
import { evaluateParsedFixedPatternSite } from "../sensor/fixed-pattern-noise.js";
|
|
26
27
|
/** Most distinct templates kept for one capture event. Any further distinct sites just run the full path. */
|
|
27
28
|
const MAXIMUM_TEMPLATES = 64;
|
|
@@ -273,7 +274,7 @@ function geometry(t, k, frame, environment, index, access) {
|
|
|
273
274
|
if (!Number.isFinite(reference.z) || reference.z <= 0)
|
|
274
275
|
return undefined;
|
|
275
276
|
const rx = pd * (reference.x / reference.z), ry = pd * (reference.y / reference.z);
|
|
276
|
-
const length =
|
|
277
|
+
const length = hypot3(rx, ry, pd);
|
|
277
278
|
const ray = { x: rx / length, y: ry / length, z: pd / length };
|
|
278
279
|
// Build the parseSceneRadianceEvaluationRequest output directly. Every other field is a template constant already
|
|
279
280
|
// parsed for the template site. The sample ID is a public opaque ID (trimming cannot change it) and the time is
|
|
@@ -281,7 +282,7 @@ function geometry(t, k, frame, environment, index, access) {
|
|
|
281
282
|
const direction = { x: -ray.x, y: -ray.y, z: -ray.z };
|
|
282
283
|
if (!Number.isFinite(direction.x) || !Number.isFinite(direction.y) || !Number.isFinite(direction.z))
|
|
283
284
|
return undefined;
|
|
284
|
-
const norm =
|
|
285
|
+
const norm = hypot3(direction.x, direction.y, direction.z);
|
|
285
286
|
if (!Number.isFinite(norm) || Math.abs(norm - 1) > 1e-9)
|
|
286
287
|
return undefined;
|
|
287
288
|
// #394: the key the evaluator would read for this request (undefined where it would throw: the full path decides).
|
|
@@ -969,6 +970,12 @@ function sensorMatches(site, photo, x, y, t, memo) {
|
|
|
969
970
|
photo.temporalSamples.length === t.photo.temporalSamples.length;
|
|
970
971
|
}
|
|
971
972
|
/** The sensor stage of calculateNativeRawSite, in the same operation order. Returns undefined when one of its checks would throw. */
|
|
973
|
+
/**
|
|
974
|
+
* #418: the charge and read-noise streams of the site leanSensorSite is computing, reseeded for each site. leanSensorSite is
|
|
975
|
+
* synchronous and never re-entered, so one pair serves every call; reseeding sets exactly the state a new
|
|
976
|
+
* createDeterministicUniformStream would start from, so every draw is unchanged.
|
|
977
|
+
*/
|
|
978
|
+
const chargeStream = createResettableUniformStream(), readNoiseStream = createResettableUniformStream();
|
|
972
979
|
function leanSensorSite(t, h, index, seed) {
|
|
973
980
|
const ratio = h.total / t.capacity, headroom = t.capacity - h.total;
|
|
974
981
|
if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
|
|
@@ -979,19 +986,23 @@ function leanSensorSite(t, h, index, seed) {
|
|
|
979
986
|
const darkMean = pattern === null ? h.darkE : h.darkE * pattern.darkCurrentFactor;
|
|
980
987
|
if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
|
|
981
988
|
return undefined;
|
|
982
|
-
const random =
|
|
989
|
+
const random = chargeStream;
|
|
990
|
+
random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
|
|
983
991
|
const photoRealized = samplePoisson(photoMean, random), darkRealized = samplePoisson(darkMean, random);
|
|
984
992
|
const realized = photoRealized + darkRealized + 0;
|
|
985
993
|
if (!Number.isFinite(realized) || realized < 0)
|
|
986
994
|
return undefined;
|
|
987
|
-
const readRandom =
|
|
995
|
+
const readRandom = readNoiseStream;
|
|
996
|
+
readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
|
|
988
997
|
const regime = t.readout.regime, capacity = t.capacity;
|
|
989
998
|
const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
|
|
990
|
-
|
|
991
|
-
|
|
999
|
+
// The components' draws and their sum in one loop: the same draws, added to 0 in the same order as before (#418).
|
|
1000
|
+
const components = regime.readNoiseComponents;
|
|
992
1001
|
let totalReadNoise = 0;
|
|
993
|
-
for (
|
|
994
|
-
|
|
1002
|
+
for (let k = 0; k < components.length; k++) {
|
|
1003
|
+
const rms = components[k].rmsElectrons.value;
|
|
1004
|
+
totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
|
|
1005
|
+
}
|
|
995
1006
|
const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
|
|
996
1007
|
const threshold = regime.preAdcSaturationElectronEquivalent.value;
|
|
997
1008
|
const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
|
|
@@ -1001,6 +1012,84 @@ function leanSensorSite(t, h, index, seed) {
|
|
|
1001
1012
|
flags: Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2,
|
|
1002
1013
|
whiteCode: t.whiteCode, photoRealized, darkRealized };
|
|
1003
1014
|
}
|
|
1015
|
+
function flatSensorStage(t) {
|
|
1016
|
+
const regime = t.readout.regime;
|
|
1017
|
+
return { darkRate: t.darkRate, capacity: t.capacity, pattern: t.pattern, whiteCode: t.whiteCode, regime, adcMaximumCode: t.readout.adcMaximumCode,
|
|
1018
|
+
inlDefined: regime.adc.integralNonLinearity !== undefined, gain: regime.systemConversionGainElectronsPerCode.value, black: regime.adc.blackLevelCode,
|
|
1019
|
+
digitalGain: regime.adc.digitalGain, digitalSaturationCode: regime.adc.digitalSaturationCode, threshold: regime.preAdcSaturationElectronEquivalent.value,
|
|
1020
|
+
rms: Float64Array.from(regime.readNoiseComponents, (component) => component.rmsElectrons.value), darkMean: NaN, darkThreshold: NaN,
|
|
1021
|
+
out: { rawCode: 0, blackLevelCode: 0, digitalSaturationCode: 0, flags: 0, whiteCode: 0 } };
|
|
1022
|
+
}
|
|
1023
|
+
/** Writes the site's outputs into `f.out`; false where leanSensorSite returns undefined. */
|
|
1024
|
+
function flatSensorSite(f, photoE, darkE, total, x, y, index, seed) {
|
|
1025
|
+
const ratio = total / f.capacity, headroom = f.capacity - total;
|
|
1026
|
+
if (!Number.isFinite(ratio) || !Number.isFinite(headroom))
|
|
1027
|
+
return false;
|
|
1028
|
+
const pattern = f.pattern === undefined ? null : evaluateParsedFixedPatternSite(f.pattern, { x, y });
|
|
1029
|
+
const photoMean = pattern === null ? photoE : photoE * pattern.photoResponseGainFactor;
|
|
1030
|
+
const darkMean = pattern === null ? darkE : darkE * pattern.darkCurrentFactor;
|
|
1031
|
+
if (!Number.isFinite(photoMean) || photoMean < 0 || !Number.isFinite(darkMean) || darkMean < 0)
|
|
1032
|
+
return false;
|
|
1033
|
+
const random = chargeStream;
|
|
1034
|
+
random.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_CHARGE_STREAM_ID));
|
|
1035
|
+
const photoRealized = samplePoisson(photoMean, random);
|
|
1036
|
+
let darkRealized;
|
|
1037
|
+
if (darkMean > 0 && darkMean < 30) {
|
|
1038
|
+
// samplePoisson's branch for 0 < mean < 30 (Knuth's product of uniforms) with its threshold Math.exp(−mean) kept while the
|
|
1039
|
+
// mean repeats: the same draws from the same stream. (createPoissonSampler would also do this, but it brings the
|
|
1040
|
+
// portable-math functions into capture-only bundles.)
|
|
1041
|
+
if (darkMean !== f.darkMean) {
|
|
1042
|
+
f.darkMean = darkMean;
|
|
1043
|
+
f.darkThreshold = Math.exp(-darkMean);
|
|
1044
|
+
}
|
|
1045
|
+
const threshold = f.darkThreshold;
|
|
1046
|
+
let product = 1, count = 0;
|
|
1047
|
+
do {
|
|
1048
|
+
count += 1;
|
|
1049
|
+
product *= random.next();
|
|
1050
|
+
} while (product > threshold);
|
|
1051
|
+
darkRealized = count - 1;
|
|
1052
|
+
}
|
|
1053
|
+
else
|
|
1054
|
+
darkRealized = samplePoisson(darkMean, random);
|
|
1055
|
+
const realized = photoRealized + darkRealized + 0;
|
|
1056
|
+
if (!Number.isFinite(realized) || realized < 0)
|
|
1057
|
+
return false;
|
|
1058
|
+
const readRandom = readNoiseStream;
|
|
1059
|
+
readRandom.reset(deriveNativeSiteStreamSeedUint32(seed, index, NATIVE_SITE_READ_NOISE_STREAM_ID));
|
|
1060
|
+
const capacity = f.capacity;
|
|
1061
|
+
const afterPhysical = Math.min(realized, capacity), physicalSaturation = afterPhysical < realized;
|
|
1062
|
+
const rmsAll = f.rms;
|
|
1063
|
+
let totalReadNoise = 0;
|
|
1064
|
+
for (let k = 0; k < rmsAll.length; k++) {
|
|
1065
|
+
const rms = rmsAll[k];
|
|
1066
|
+
totalReadNoise = totalReadNoise + (rms === 0 ? 0 : sampleDeterministicStandardNormal(readRandom) * rms);
|
|
1067
|
+
}
|
|
1068
|
+
const afterReadNoise = afterPhysical + totalReadNoise + (pattern === null ? 0 : pattern.readoutOffsetElectrons);
|
|
1069
|
+
const threshold = f.threshold;
|
|
1070
|
+
const afterPreAdc = Math.min(afterReadNoise, threshold), preAdcSaturation = afterReadNoise > threshold;
|
|
1071
|
+
let quantized;
|
|
1072
|
+
if (f.inlDefined)
|
|
1073
|
+
quantized = adcTransfer(f.regime, f.adcMaximumCode, afterPreAdc).quantized;
|
|
1074
|
+
else {
|
|
1075
|
+
// adcTransfer without integral non-linearity: ideal = e/G + black, plus the zero INL offset, rounded half up (Math.round + 0),
|
|
1076
|
+
// then with a digital gain the ADC clamp and the gain's own rounding about the black level.
|
|
1077
|
+
const ideal = afterPreAdc / f.gain + f.black, quantizedAdc = Math.round(ideal + 0) + 0, digitalGain = f.digitalGain;
|
|
1078
|
+
if (digitalGain === undefined)
|
|
1079
|
+
quantized = quantizedAdc;
|
|
1080
|
+
else {
|
|
1081
|
+
const adcCode = Math.max(0, Math.min(quantizedAdc, f.adcMaximumCode));
|
|
1082
|
+
quantized = Math.round((adcCode - f.black) * digitalGain + f.black) + 0;
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
const digitalSaturationCode = f.digitalSaturationCode, out = f.out;
|
|
1086
|
+
out.rawCode = Math.max(0, Math.min(quantized, digitalSaturationCode));
|
|
1087
|
+
out.blackLevelCode = f.black;
|
|
1088
|
+
out.digitalSaturationCode = digitalSaturationCode;
|
|
1089
|
+
out.flags = Number(physicalSaturation) | Number(preAdcSaturation) << 1 | Number(quantized >= digitalSaturationCode) << 2;
|
|
1090
|
+
out.whiteCode = f.whiteCode;
|
|
1091
|
+
return true;
|
|
1092
|
+
}
|
|
1004
1093
|
/** The Kahan (compensated) total that composeSensorAccumulatedCharge computes, for photo and dark charge only. */
|
|
1005
1094
|
function chargeTotal(photoE, darkE) {
|
|
1006
1095
|
let sum = 0, correction = 0;
|
|
@@ -1029,17 +1118,22 @@ export function createVerifiedNativeSiteReadout(parsed, result, whiteCode, photo
|
|
|
1029
1118
|
realization.additionalComponents.length !== 0 || out.rawCode !== r.rawCode || out.blackLevelCode !== r.blackLevelCode || out.digitalSaturationCode !== r.digitalSaturationCode ||
|
|
1030
1119
|
out.flags !== (Number(r.physicalScalarSaturationApplied) | Number(r.preAdcSaturationApplied) << 1 | Number(r.digitalSaturationApplied) << 2))
|
|
1031
1120
|
return undefined;
|
|
1121
|
+
// #449: the flat stage must reproduce the verification site too.
|
|
1122
|
+
const flat = flatSensorStage(t), fo = flat.out;
|
|
1123
|
+
if (!flatSensorSite(flat, photo.expectedGeneratedElectronCount, darkE, total, x, y, index, seed) || fo.rawCode !== out.rawCode || fo.blackLevelCode !== out.blackLevelCode ||
|
|
1124
|
+
fo.digitalSaturationCode !== out.digitalSaturationCode || fo.flags !== out.flags || fo.whiteCode !== out.whiteCode)
|
|
1125
|
+
return undefined;
|
|
1032
1126
|
return {
|
|
1033
1127
|
compute(photoE, duration, sx, sy, siteIndex, siteSeed) {
|
|
1034
1128
|
// The parse-phase predicates of the full path (claim()): finite non-negative dark and total charge.
|
|
1035
|
-
const siteDark =
|
|
1129
|
+
const siteDark = flat.darkRate * duration;
|
|
1036
1130
|
if (!Number.isFinite(photoE) || photoE < 0 || !Number.isFinite(siteDark) || siteDark < 0)
|
|
1037
1131
|
return undefined;
|
|
1038
1132
|
const siteTotal = chargeTotal(photoE, siteDark);
|
|
1039
1133
|
if (!Number.isFinite(siteTotal) || siteTotal < 0)
|
|
1040
1134
|
return undefined;
|
|
1041
|
-
|
|
1042
|
-
return
|
|
1135
|
+
// One record for every site (#449): the caller copies the five RAW fields before its next call.
|
|
1136
|
+
return flatSensorSite(flat, photoE, siteDark, siteTotal, sx, sy, siteIndex, siteSeed) ? fo : undefined;
|
|
1043
1137
|
}
|
|
1044
1138
|
};
|
|
1045
1139
|
}
|