@photivra/engine 2.5.0 → 2.7.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 +129 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +1 -0
- package/dist/capture/native-environment-lean.d.ts +1 -1
- package/dist/capture/native-environment-lean.js +17 -3
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-raw.d.ts +0 -4
- package/dist/capture/native-environment-raw.js +6 -2
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/color/illuminants.d.ts +15 -0
- package/dist/color/illuminants.js +81 -24
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/srgb-transfer.d.ts +6 -0
- package/dist/color/srgb-transfer.js +9 -0
- package/dist/color/srgb-transfer.js.map +1 -1
- package/dist/color/white-balance.d.ts +6 -0
- package/dist/color/white-balance.js +15 -2
- package/dist/color/white-balance.js.map +1 -1
- package/dist/core/internal-radiance-evaluator.d.ts +6 -0
- package/dist/core/internal-radiance-evaluator.js +33 -0
- package/dist/core/internal-radiance-evaluator.js.map +1 -0
- package/dist/core/portable-math.d.ts +33 -0
- package/dist/core/portable-math.js +342 -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/chart-radiance-batch.js +3 -2
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +11 -0
- package/dist/facade/chart-scenes.js +21 -7
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +10 -0
- package/dist/facade/diffraction-provider.js +38 -17
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +213 -0
- package/dist/facade/display-view.js +1543 -0
- package/dist/facade/display-view.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +45 -5
- package/dist/facade/scene-radiance.js +97 -28
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +233 -5
- package/dist/facade/simulate-capture.js +120 -37
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +45 -0
- package/dist/facade/simulate-display-view.js +214 -0
- package/dist/facade/simulate-display-view.js.map +1 -0
- package/dist/facade/subject-framing.d.ts +16 -0
- package/dist/facade/subject-framing.js +8 -2
- package/dist/facade/subject-framing.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +9 -2
- package/dist/optics/diffraction-psf-profile.js +33 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/irradiance-convolution.js +1 -1
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js +10 -5
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/print-plan.d.ts +8 -1
- package/dist/output/print-plan.js +8 -1
- package/dist/output/print-plan.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +16 -0
- package/dist/schema/scene-radiance.js +25 -4
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +12 -0
- package/dist/sensor/raw-readout.js +131 -9
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +13 -0
- package/dist/sensor/stochastic-seed.js +42 -8
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +19 -1
- package/docs/QUICK_START.md +203 -4
- package/docs/USAGE.md +15 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +52 -2
- package/docs/api/facade-scene-radiance.md +34 -1
- package/docs/api/facade-simulate-capture.md +66 -2
- package/docs/api/facade-simulate-display-view.md +57 -0
- package/docs/api/facade-subject-framing.md +16 -0
- package/docs/api/output-print-plan.md +8 -1
- package/package.json +3 -2
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* correlation tests, not this hash alone, show that the resulting noise is statistically acceptable.
|
|
17
17
|
*/
|
|
18
18
|
import { InvalidScientificInputError } from "../core/validation.js";
|
|
19
|
+
import { portableCosTwoPi, portableLog, portableSinTwoPi } from "../core/portable-math.js";
|
|
19
20
|
/** Stream IDs. Changing them changes every simulated RAW code. */
|
|
20
21
|
export const NATIVE_SITE_CHARGE_STREAM_ID = 0;
|
|
21
22
|
export const NATIVE_SITE_READ_NOISE_STREAM_ID = 1;
|
|
@@ -69,17 +70,21 @@ export function deriveNativeSiteStreamSeedUint32(captureSeedUint32, nativeSiteIn
|
|
|
69
70
|
* Changing any of this changes simulated RAW output and needs a model and replay review.
|
|
70
71
|
*/
|
|
71
72
|
export function createDeterministicUniformStream(seedUint32) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
73
|
+
// The state lives in a Uint32Array: storing the int32 result there is the same `>>> 0` reduction, and a uint32 above
|
|
74
|
+
// 2^30 kept in a closure variable would be boxed on every step (half the speed). The draws are unchanged.
|
|
75
|
+
const state = new Uint32Array(1);
|
|
76
|
+
state[0] = fmix32((seedUint32 ^ 0x9e3779b9) >>> 0);
|
|
77
|
+
if (state[0] === 0) {
|
|
78
|
+
state[0] = 0x6d2b79f5;
|
|
75
79
|
}
|
|
76
80
|
return {
|
|
77
81
|
next() {
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
let x = state[0];
|
|
83
|
+
x ^= x << 13;
|
|
84
|
+
x ^= x >>> 17;
|
|
85
|
+
x ^= x << 5;
|
|
86
|
+
state[0] = x;
|
|
87
|
+
return (state[0] + 0.5) / 0x1_0000_0000;
|
|
83
88
|
}
|
|
84
89
|
};
|
|
85
90
|
}
|
|
@@ -93,6 +98,35 @@ export function sampleDeterministicStandardNormal(random) {
|
|
|
93
98
|
const u2 = random.next();
|
|
94
99
|
return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
95
100
|
}
|
|
101
|
+
/**
|
|
102
|
+
* The same Box–Muller draw (same two uniforms, same order) with the engine's portable logarithm and cosine
|
|
103
|
+
* (`portableLog`, `portableCosTwoPi`), so the sample has the same bits in every JavaScript engine. Used by the
|
|
104
|
+
* display view; the native noise keeps `sampleDeterministicStandardNormal`.
|
|
105
|
+
*/
|
|
106
|
+
export function samplePortableStandardNormal(random) {
|
|
107
|
+
const u1 = random.next();
|
|
108
|
+
const u2 = random.next();
|
|
109
|
+
return Math.sqrt(-2 * portableLog(u1)) * portableCosTwoPi(u2);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Standard normal samples from one stream, two per pair of uniforms: both Box–Muller outputs, r·cos(2πu₂) and then
|
|
113
|
+
* r·sin(2πu₂) with r = √(−2 ln u₁) (Box & Muller 1958; the two are independent standard normals), with the portable
|
|
114
|
+
* functions. Each call returns the next sample; the stream advances by two uniforms on every other call. Used by the
|
|
115
|
+
* display view's AHD correction states, which draw many normals from one stream.
|
|
116
|
+
*/
|
|
117
|
+
export function createPortableStandardNormalPairs(random) {
|
|
118
|
+
let spare = 0, hasSpare = false;
|
|
119
|
+
return () => {
|
|
120
|
+
if (hasSpare) {
|
|
121
|
+
hasSpare = false;
|
|
122
|
+
return spare;
|
|
123
|
+
}
|
|
124
|
+
const u1 = random.next(), u2 = random.next(), r = Math.sqrt(-2 * portableLog(u1));
|
|
125
|
+
spare = r * portableSinTwoPi(u2);
|
|
126
|
+
hasSpare = true;
|
|
127
|
+
return r * portableCosTwoPi(u2);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
96
130
|
/**
|
|
97
131
|
* Fixed-pattern stream IDs. They seed spatial patterns that belong to one physical sensor, never
|
|
98
132
|
* per-capture noise, and are hashed with the sensor-instance seed instead of the capture seed.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stochastic-seed.js","sourceRoot":"","sources":["../../src/sensor/stochastic-seed.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Seeds for the random-number streams behind simulated sensor noise. The same inputs always give\n * the same noise.\n *\n * Each per-site stream seed is a strong 32-bit hash of the capture seed, the absolute native\n * row-major site index and a fixed stream ID. The hash is MurmurHash3_x86_32 (Austin Appleby,\n * placed in the public domain) of the 12-byte little-endian message `[captureSeed, siteIndex,\n * streamId]` with hash seed 0. Its avalanche finalizer (`fmix32`) makes a one-bit input change flip\n * each output bit with probability close to 1/2. So neighbouring sites, the two streams of one site\n * and captures with nearby seeds get unrelated seeds. A linear schedule such as `seed + 2*index` fed\n * into a linear generator (xorshift) lacks this property, and produced correlated noise with too\n * little variance (issue #270).\n *\n * This is deterministic and not cryptographic; it does not claim unpredictability. The moment and\n * correlation tests, not this hash alone, show that the resulting noise is statistically acceptable.\n */\n\nimport { InvalidScientificInputError } from \"../core/validation.js\";\n\n/** Stream IDs. Changing them changes every simulated RAW code. */\nexport const NATIVE_SITE_CHARGE_STREAM_ID = 0 as const;\nexport const NATIVE_SITE_READ_NOISE_STREAM_ID = 1 as const;\n\nconst MURMUR3_C1 = 0xcc9e2d51;\nconst MURMUR3_C2 = 0x1b873593;\n\n/** MurmurHash3 32-bit avalanche finalizer; a bijection on uint32 with fmix32(0) = 0. */\nexport function fmix32(value: number): number {\n let h = value >>> 0;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\nfunction mixBlock(hash: number, block: number): number {\n let k = Math.imul(block >>> 0, MURMUR3_C1);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, MURMUR3_C2);\n let h = (hash ^ k) >>> 0;\n h = (h << 13) | (h >>> 19);\n return (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n}\n\nfunction isUint32(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffffffff;\n}\n\n/**\n * Returns the uint32 seed of one random stream for one absolute native site.\n * All inputs must be uint32 integers. The site index is the full-frame native row-major\n * index, never relative to a tile or chunk, so how the work is split cannot change it.\n */\nexport function deriveNativeSiteStreamSeedUint32(\n captureSeedUint32: number,\n nativeSiteIndex: number,\n streamId: number\n): number {\n if (!isUint32(captureSeedUint32) || !isUint32(nativeSiteIndex) || !isUint32(streamId)) {\n throw new InvalidScientificInputError(\"Stochastic stream seed inputs must be uint32 integers.\");\n }\n let h = 0;\n h = mixBlock(h, captureSeedUint32);\n h = mixBlock(h, nativeSiteIndex);\n h = mixBlock(h, streamId);\n return fmix32(h ^ 12);\n}\n\n/** One deterministic stream of uniforms strictly inside (0,1). */\nexport interface DeterministicUniformStream {\n next(): number;\n}\n\n/**\n * Creates the deterministic, non-cryptographic xorshift32 random stream for one charge, read-noise\n * or fixed-pattern identity.\n *\n * The seed is first scrambled by the MurmurHash3 `fmix32` avalanche finalizer. xorshift32 is linear\n * over GF(2), so an unscrambled seed would give nearby seeds almost identical first draws, and noise\n * that is correlated across the sensor with too little variance (issue #270). The nonzero fallback\n * avoids the zero state, which xorshift can never leave. Adding 0.5 before dividing gives values\n * strictly inside (0,1), so the logarithms in the Poisson and normal transforms never see 0 or 1.\n * Changing any of this changes simulated RAW output and needs a model and replay review.\n */\nexport function createDeterministicUniformStream(seedUint32: number): DeterministicUniformStream {\n let state = fmix32((seedUint32 ^ 0x9e3779b9) >>> 0);\n if (state === 0) {\n state = 0x6d2b79f5;\n }\n return {\n next(): number {\n state ^= state << 13;\n state ^= state >>> 17;\n state ^= state << 5;\n state >>>= 0;\n return (state + 0.5) / 0x1_0000_0000;\n }\n };\n}\n\n/**\n * Box–Muller transform of two uniform values in (0,1) (Box & Muller 1958). Each call uses two values\n * and returns one standard normal sample. The second possible sample is not cached, so the order of\n * draws from the stream never shifts.\n */\nexport function sampleDeterministicStandardNormal(random: DeterministicUniformStream): number {\n const u1 = random.next();\n const u2 = random.next();\n return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n/**\n * Fixed-pattern stream IDs. They seed spatial patterns that belong to one physical sensor, never\n * per-capture noise, and are hashed with the sensor-instance seed instead of the capture seed.\n */\nexport const SENSOR_FIXED_PATTERN_PRNU_STREAM_ID = 16 as const;\nexport const SENSOR_FIXED_PATTERN_DSNU_STREAM_ID = 17 as const;\nexport const SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID = 18 as const;\nexport const SENSOR_FIXED_PATTERN_ROW_STREAM_ID = 19 as const;\nexport const SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID = 20 as const;\n\n/**\n * Returns the uint32 seed of one fixed-pattern stream for one physical colour-sampling site: the\n * MurmurHash3_x86_32 hash (hash seed 0) of the 16-byte little-endian message\n * `[sensorInstanceSeed, siteX, siteY, streamId]`. Site coordinates are absolute indices in the\n * colour-sampling grid, so the pattern belongs to the physical sensor and does not change with crop,\n * tiling or the per-capture seed. Deterministic and not cryptographic.\n */\nexport function deriveSensorFixedPatternSeedUint32(\n sensorInstanceSeedUint32: number,\n siteX: number,\n siteY: number,\n streamId: number\n): number {\n if (!isUint32(sensorInstanceSeedUint32) || !isUint32(siteX) || !isUint32(siteY) || !isUint32(streamId)) {\n throw new InvalidScientificInputError(\"Fixed-pattern seed inputs must be uint32 integers.\");\n }\n let h = 0;\n h = mixBlock(h, sensorInstanceSeedUint32);\n h = mixBlock(h, siteX);\n h = mixBlock(h, siteY);\n h = mixBlock(h, streamId);\n return fmix32(h ^ 16);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stochastic-seed.js","sourceRoot":"","sources":["../../src/sensor/stochastic-seed.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Seeds for the random-number streams behind simulated sensor noise. The same inputs always give\n * the same noise.\n *\n * Each per-site stream seed is a strong 32-bit hash of the capture seed, the absolute native\n * row-major site index and a fixed stream ID. The hash is MurmurHash3_x86_32 (Austin Appleby,\n * placed in the public domain) of the 12-byte little-endian message `[captureSeed, siteIndex,\n * streamId]` with hash seed 0. Its avalanche finalizer (`fmix32`) makes a one-bit input change flip\n * each output bit with probability close to 1/2. So neighbouring sites, the two streams of one site\n * and captures with nearby seeds get unrelated seeds. A linear schedule such as `seed + 2*index` fed\n * into a linear generator (xorshift) lacks this property, and produced correlated noise with too\n * little variance (issue #270).\n *\n * This is deterministic and not cryptographic; it does not claim unpredictability. The moment and\n * correlation tests, not this hash alone, show that the resulting noise is statistically acceptable.\n */\n\nimport { InvalidScientificInputError } from \"../core/validation.js\";\nimport { portableCosTwoPi, portableLog, portableSinTwoPi } from \"../core/portable-math.js\";\n\n/** Stream IDs. Changing them changes every simulated RAW code. */\nexport const NATIVE_SITE_CHARGE_STREAM_ID = 0 as const;\nexport const NATIVE_SITE_READ_NOISE_STREAM_ID = 1 as const;\n\nconst MURMUR3_C1 = 0xcc9e2d51;\nconst MURMUR3_C2 = 0x1b873593;\n\n/** MurmurHash3 32-bit avalanche finalizer; a bijection on uint32 with fmix32(0) = 0. */\nexport function fmix32(value: number): number {\n let h = value >>> 0;\n h ^= h >>> 16;\n h = Math.imul(h, 0x85ebca6b);\n h ^= h >>> 13;\n h = Math.imul(h, 0xc2b2ae35);\n h ^= h >>> 16;\n return h >>> 0;\n}\n\nfunction mixBlock(hash: number, block: number): number {\n let k = Math.imul(block >>> 0, MURMUR3_C1);\n k = (k << 15) | (k >>> 17);\n k = Math.imul(k, MURMUR3_C2);\n let h = (hash ^ k) >>> 0;\n h = (h << 13) | (h >>> 19);\n return (Math.imul(h, 5) + 0xe6546b64) >>> 0;\n}\n\nfunction isUint32(value: number): boolean {\n return Number.isInteger(value) && value >= 0 && value <= 0xffffffff;\n}\n\n/**\n * Returns the uint32 seed of one random stream for one absolute native site.\n * All inputs must be uint32 integers. The site index is the full-frame native row-major\n * index, never relative to a tile or chunk, so how the work is split cannot change it.\n */\nexport function deriveNativeSiteStreamSeedUint32(\n captureSeedUint32: number,\n nativeSiteIndex: number,\n streamId: number\n): number {\n if (!isUint32(captureSeedUint32) || !isUint32(nativeSiteIndex) || !isUint32(streamId)) {\n throw new InvalidScientificInputError(\"Stochastic stream seed inputs must be uint32 integers.\");\n }\n let h = 0;\n h = mixBlock(h, captureSeedUint32);\n h = mixBlock(h, nativeSiteIndex);\n h = mixBlock(h, streamId);\n return fmix32(h ^ 12);\n}\n\n/** One deterministic stream of uniforms strictly inside (0,1). */\nexport interface DeterministicUniformStream {\n next(): number;\n}\n\n/**\n * Creates the deterministic, non-cryptographic xorshift32 random stream for one charge, read-noise\n * or fixed-pattern identity.\n *\n * The seed is first scrambled by the MurmurHash3 `fmix32` avalanche finalizer. xorshift32 is linear\n * over GF(2), so an unscrambled seed would give nearby seeds almost identical first draws, and noise\n * that is correlated across the sensor with too little variance (issue #270). The nonzero fallback\n * avoids the zero state, which xorshift can never leave. Adding 0.5 before dividing gives values\n * strictly inside (0,1), so the logarithms in the Poisson and normal transforms never see 0 or 1.\n * Changing any of this changes simulated RAW output and needs a model and replay review.\n */\nexport function createDeterministicUniformStream(seedUint32: number): DeterministicUniformStream {\n // The state lives in a Uint32Array: storing the int32 result there is the same `>>> 0` reduction, and a uint32 above\n // 2^30 kept in a closure variable would be boxed on every step (half the speed). The draws are unchanged.\n const state = new Uint32Array(1);\n state[0] = fmix32((seedUint32 ^ 0x9e3779b9) >>> 0);\n if (state[0] === 0) {\n state[0] = 0x6d2b79f5;\n }\n return {\n next(): number {\n let x = state[0]!;\n x ^= x << 13;\n x ^= x >>> 17;\n x ^= x << 5;\n state[0] = x;\n return (state[0] + 0.5) / 0x1_0000_0000;\n }\n };\n}\n\n/**\n * Box–Muller transform of two uniform values in (0,1) (Box & Muller 1958). Each call uses two values\n * and returns one standard normal sample. The second possible sample is not cached, so the order of\n * draws from the stream never shifts.\n */\nexport function sampleDeterministicStandardNormal(random: DeterministicUniformStream): number {\n const u1 = random.next();\n const u2 = random.next();\n return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\n/**\n * The same Box–Muller draw (same two uniforms, same order) with the engine's portable logarithm and cosine\n * (`portableLog`, `portableCosTwoPi`), so the sample has the same bits in every JavaScript engine. Used by the\n * display view; the native noise keeps `sampleDeterministicStandardNormal`.\n */\nexport function samplePortableStandardNormal(random: DeterministicUniformStream): number {\n const u1 = random.next();\n const u2 = random.next();\n return Math.sqrt(-2 * portableLog(u1)) * portableCosTwoPi(u2);\n}\n\n/**\n * Standard normal samples from one stream, two per pair of uniforms: both Box–Muller outputs, r·cos(2πu₂) and then\n * r·sin(2πu₂) with r = √(−2 ln u₁) (Box & Muller 1958; the two are independent standard normals), with the portable\n * functions. Each call returns the next sample; the stream advances by two uniforms on every other call. Used by the\n * display view's AHD correction states, which draw many normals from one stream.\n */\nexport function createPortableStandardNormalPairs(random: DeterministicUniformStream): () => number {\n let spare = 0, hasSpare = false;\n return () => {\n if (hasSpare) { hasSpare = false; return spare; }\n const u1 = random.next(), u2 = random.next(), r = Math.sqrt(-2 * portableLog(u1));\n spare = r * portableSinTwoPi(u2); hasSpare = true;\n return r * portableCosTwoPi(u2);\n };\n}\n\n/**\n * Fixed-pattern stream IDs. They seed spatial patterns that belong to one physical sensor, never\n * per-capture noise, and are hashed with the sensor-instance seed instead of the capture seed.\n */\nexport const SENSOR_FIXED_PATTERN_PRNU_STREAM_ID = 16 as const;\nexport const SENSOR_FIXED_PATTERN_DSNU_STREAM_ID = 17 as const;\nexport const SENSOR_FIXED_PATTERN_HOT_PIXEL_STREAM_ID = 18 as const;\nexport const SENSOR_FIXED_PATTERN_ROW_STREAM_ID = 19 as const;\nexport const SENSOR_FIXED_PATTERN_COLUMN_STREAM_ID = 20 as const;\n\n/**\n * Returns the uint32 seed of one fixed-pattern stream for one physical colour-sampling site: the\n * MurmurHash3_x86_32 hash (hash seed 0) of the 16-byte little-endian message\n * `[sensorInstanceSeed, siteX, siteY, streamId]`. Site coordinates are absolute indices in the\n * colour-sampling grid, so the pattern belongs to the physical sensor and does not change with crop,\n * tiling or the per-capture seed. Deterministic and not cryptographic.\n */\nexport function deriveSensorFixedPatternSeedUint32(\n sensorInstanceSeedUint32: number,\n siteX: number,\n siteY: number,\n streamId: number\n): number {\n if (!isUint32(sensorInstanceSeedUint32) || !isUint32(siteX) || !isUint32(siteY) || !isUint32(streamId)) {\n throw new InvalidScientificInputError(\"Fixed-pattern seed inputs must be uint32 integers.\");\n }\n let h = 0;\n h = mixBlock(h, sensorInstanceSeedUint32);\n h = mixBlock(h, siteX);\n h = mixBlock(h, siteY);\n h = mixBlock(h, streamId);\n return fmix32(h ^ 16);\n}\n"]}
|
package/docs/AGENT_GUIDE.md
CHANGED
|
@@ -20,6 +20,7 @@ one-call capture, see the [Quick Start](QUICK_START.md). For every export, see t
|
|
|
20
20
|
| Task | Call | Notes |
|
|
21
21
|
| --- | --- | --- |
|
|
22
22
|
| Simulate a full photo | [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) | Async. Returns an image, the RAW data, `assumptions` and `provenance`. |
|
|
23
|
+
| Preview a whole frame as the real capture resized for display | [`simulateDisplayView`](api/facade-simulate-display-view.md#simulatedisplayview) | Async. Takes the capture's request plus a display size; costs follow the display pixels. Labelled `kind: "display-view"`: a preview, never a capture. |
|
|
23
24
|
| Exposure value (EV) of a setting | [`calculateExposureValue100`](api/exposure-exposure.md#calculateexposurevalue100) | EV at ISO 100 = log2(N²/t). |
|
|
24
25
|
| Keep brightness after changing aperture or shutter | [`calculateEquivalentIso`](api/exposure-exposure.md#calculateequivalentiso) | Returns the compensating ISO. Not a noise model. |
|
|
25
26
|
| Compare two exposures in stops | [`calculateRelativeRenderedExposure`](api/exposure-exposure.md#calculaterelativerenderedexposure) | Includes ISO as gain. |
|
package/docs/API_REFERENCE.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Public API reference
|
|
2
2
|
|
|
3
|
-
Generated from the exact root exports for **@photivra/engine 2.
|
|
3
|
+
Generated from the exact root exports for **@photivra/engine 2.7.0 / root contract 2.7.0** by `node scripts/generate-api-reference.mjs`. No module deep import is supported.
|
|
4
4
|
|
|
5
5
|
Use [the developer guide](https://github.com/Photivra/photivra/blob/main/docs/DEVELOPERS.md) for executable paths and scientific boundaries. Types are contracts, not runtime validation: parse untrusted data, preserve units/reference frames, read assessment blockers, and retain evidence/limitations. Optional properties do not imply a universal default. Model/schema IDs remain independent of the distribution.
|
|
6
6
|
|
|
@@ -11,6 +11,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
11
11
|
| Task | Functions |
|
|
12
12
|
| --- | --- |
|
|
13
13
|
| Simulate a full photo (async) | [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) |
|
|
14
|
+
| Preview a whole frame as the real capture resized (async) | [`simulateDisplayView`](api/facade-simulate-display-view.md#simulatedisplayview) |
|
|
14
15
|
| Exposure value and equivalent settings | [`calculateExposureValue100`](api/exposure-exposure.md#calculateexposurevalue100), [`calculateEquivalentIso`](api/exposure-exposure.md#calculateequivalentiso), [`calculateRelativeRenderedExposure`](api/exposure-exposure.md#calculaterelativerenderedexposure) |
|
|
15
16
|
| Depth of field, hyperfocal distance and defocus blur | [`calculateDepthOfField`](api/optics-depth-of-field.md#calculatedepthoffield), [`calculateDefocusCircle`](api/optics-depth-of-field.md#calculatedefocuscircle) |
|
|
16
17
|
| Field of view | [`calculateFieldOfView`](api/camera-field-of-view.md#calculatefieldofview) |
|
|
@@ -1161,6 +1162,8 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1161
1162
|
|
|
1162
1163
|
| Export | Kind |
|
|
1163
1164
|
| --- | --- |
|
|
1165
|
+
| [`CALLER_MATERIAL_SCENE_MAX_MATERIALS`](api/facade-scene-radiance.md#caller_material_scene_max_materials) | Runtime |
|
|
1166
|
+
| [`CallerMaterialScene`](api/facade-scene-radiance.md#callermaterialscene) | Type |
|
|
1164
1167
|
| [`CallerRadianceScene`](api/facade-scene-radiance.md#callerradiancescene) | Type |
|
|
1165
1168
|
| [`CallerReflectanceScene`](api/facade-scene-radiance.md#callerreflectancescene) | Type |
|
|
1166
1169
|
| [`CaptureIlluminant`](api/facade-scene-radiance.md#captureilluminant) | Type |
|
|
@@ -1171,6 +1174,7 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1171
1174
|
| [`PlanarSceneRadianceProvider`](api/facade-scene-radiance.md#planarsceneradianceprovider) | Type |
|
|
1172
1175
|
| [`resolveCaptureIlluminant`](api/facade-scene-radiance.md#resolvecaptureilluminant) | Runtime |
|
|
1173
1176
|
| [`ResolvedCaptureIlluminant`](api/facade-scene-radiance.md#resolvedcaptureilluminant) | Type |
|
|
1177
|
+
| [`SceneMaterialPoint`](api/facade-scene-radiance.md#scenematerialpoint) | Type |
|
|
1174
1178
|
| [`ScenePlanePoint`](api/facade-scene-radiance.md#sceneplanepoint) | Type |
|
|
1175
1179
|
|
|
1176
1180
|
## facade/simulate-capture.ts
|
|
@@ -1179,11 +1183,13 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1179
1183
|
|
|
1180
1184
|
| Export | Kind |
|
|
1181
1185
|
| --- | --- |
|
|
1186
|
+
| [`DisplayViewResult`](api/facade-simulate-capture.md#displayviewresult) | Type |
|
|
1182
1187
|
| [`IdealReferenceResult`](api/facade-simulate-capture.md#idealreferenceresult) | Type |
|
|
1183
1188
|
| [`SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES`](api/facade-simulate-capture.md#simulate_capture_box_aperture_default_samples) | Runtime |
|
|
1184
1189
|
| [`SIMULATE_CAPTURE_COST_OPTIONS`](api/facade-simulate-capture.md#simulate_capture_cost_options) | Runtime |
|
|
1185
1190
|
| [`SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX`](api/facade-simulate-capture.md#simulate_capture_default_illuminance_lux) | Runtime |
|
|
1186
1191
|
| [`SIMULATE_CAPTURE_FACADE_VERSION`](api/facade-simulate-capture.md#simulate_capture_facade_version) | Runtime |
|
|
1192
|
+
| [`SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS`](api/facade-simulate-capture.md#simulate_display_view_default_samples_per_axis) | Runtime |
|
|
1187
1193
|
| [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) | Runtime |
|
|
1188
1194
|
| [`SimulateCaptureAssumption`](api/facade-simulate-capture.md#simulatecaptureassumption) | Type |
|
|
1189
1195
|
| [`SimulateCaptureBorderHandling`](api/facade-simulate-capture.md#simulatecaptureborderhandling) | Type |
|
|
@@ -1200,8 +1206,20 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1200
1206
|
| [`SimulateCaptureResult`](api/facade-simulate-capture.md#simulatecaptureresult) | Type |
|
|
1201
1207
|
| [`SimulateCaptureSensorAntiAliasing`](api/facade-simulate-capture.md#simulatecapturesensorantialiasing) | Type |
|
|
1202
1208
|
| [`SimulateCaptureWhiteBalance`](api/facade-simulate-capture.md#simulatecapturewhitebalance) | Type |
|
|
1209
|
+
| [`SimulateDisplayViewOptions`](api/facade-simulate-capture.md#simulatedisplayviewoptions) | Type |
|
|
1203
1210
|
| [`simulateIdealReference`](api/facade-simulate-capture.md#simulateidealreference) | Runtime |
|
|
1204
1211
|
|
|
1212
|
+
## facade/simulate-display-view.ts
|
|
1213
|
+
|
|
1214
|
+
[Detailed contracts](api/facade-simulate-display-view.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/simulate-display-view.ts)
|
|
1215
|
+
|
|
1216
|
+
| Export | Kind |
|
|
1217
|
+
| --- | --- |
|
|
1218
|
+
| [`PreparedDisplayView`](api/facade-simulate-display-view.md#prepareddisplayview) | Type |
|
|
1219
|
+
| [`PreparedDisplayViewRenderOptions`](api/facade-simulate-display-view.md#prepareddisplayviewrenderoptions) | Type |
|
|
1220
|
+
| [`prepareDisplayView`](api/facade-simulate-display-view.md#preparedisplayview) | Runtime |
|
|
1221
|
+
| [`simulateDisplayView`](api/facade-simulate-display-view.md#simulatedisplayview) | Runtime |
|
|
1222
|
+
|
|
1205
1223
|
## facade/subject-framing.ts
|
|
1206
1224
|
|
|
1207
1225
|
[Detailed contracts](api/facade-subject-framing.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/subject-framing.ts)
|
package/docs/QUICK_START.md
CHANGED
|
@@ -233,6 +233,16 @@ const coverage = calculateChartFraming({ sensor: { widthMm: 36, heightMm: 24 },
|
|
|
233
233
|
console.log(coverage.coverage.frameWidthFraction, coverage.magnification, coverage.belowMinimumFocus);
|
|
234
234
|
```
|
|
235
235
|
|
|
236
|
+
**Close-up light (2.6).** Focused close, the lens delivers less light: the working f-number is N·(1 + m) with m = v/s at
|
|
237
|
+
the focus distance (ideal symmetric thin lens, unity pupil magnification), so the image-plane irradiance falls by
|
|
238
|
+
1/(1 + m)² compared with the same f-number at infinity focus. `calculateChartFraming` and
|
|
239
|
+
`calculateSubjectDistanceForFraming` report it as `closeUpTransmission` (1/(1 + m)²) and `closeUpLossStops` (2·log₂(1 + m),
|
|
240
|
+
how many stops darker). Both come from the same function as the factor `simulateCapture` applies in the scene-to-sensor
|
|
241
|
+
irradiance, so they cannot drift apart; a test checks the RAW signal ratio of a uniform chart at 0.35 m and 50 m against
|
|
242
|
+
them within 0.5 %. With the 50 mm prime: 0.81 (0.30 stop darker) at 0.5 m, 0.90 (0.15 stop) at 1 m, 0.95 (0.07 stop) at
|
|
243
|
+
2 m and 0.98 (0.03 stop) at 5 m. Show it to users as "about N stops darker at this distance"; it is the thin-lens
|
|
244
|
+
model, not a measured lens (real lenses with non-unity pupil magnification differ).
|
|
245
|
+
|
|
236
246
|
Tests check that at `calculateSubjectDistanceForFraming`'s distance the coverage is 1 on the fitted side (every fit,
|
|
237
247
|
three sensor shapes, four primes), and that at any distance it equals `result.scene.chart` from `simulateCapture` with
|
|
238
248
|
`chartSize` (magnification, both coverage fractions, the chart's image size and the framed size, within 1e-12
|
|
@@ -292,6 +302,10 @@ What changes and what does not:
|
|
|
292
302
|
|
|
293
303
|
Requests at 0.5–100 µm are byte-identical to 2.4.1.
|
|
294
304
|
|
|
305
|
+
A whole-frame view simulates a camera with giant pixels. For a preview that looks like the real camera's photo
|
|
306
|
+
shrunk to the frame size (the real pixel pitch for the optics, the real mosaic and the noise of the averaged real
|
|
307
|
+
pixels), use [Display view](#display-view) instead.
|
|
308
|
+
|
|
295
309
|
### ISO and pixel size
|
|
296
310
|
|
|
297
311
|
The tier's ISO signal chain (conversion gain, saturation signal and read noise per ISO setting) is
|
|
@@ -440,6 +454,34 @@ const vignette = await simulateCapture({
|
|
|
440
454
|
console.log(vignette.request.whiteBalanceState?.source);
|
|
441
455
|
```
|
|
442
456
|
|
|
457
|
+
**Piecewise-constant scenes (2.7).** A scene made of a few uniform reflectances, such as a chart with a printed label,
|
|
458
|
+
can be passed as `{ kind: "material-function", sceneId, materials, materialAt }`. `materials` lists reflectances as a
|
|
459
|
+
chart's patches take them: a neutral number in [0, 1], or a reflectance spectrum covering 380–780 nm. `materialAt`
|
|
460
|
+
receives the plane point without a wavelength and returns the index of its material.
|
|
461
|
+
|
|
462
|
+
The radiance at each point is the same as a reflectance function returning that material's reflectance. Captures and
|
|
463
|
+
ideal references are therefore byte-identical to the reflectance-function form. The display view, though, tabulates
|
|
464
|
+
each material once and takes the chart presets' path.
|
|
465
|
+
|
|
466
|
+
```ts
|
|
467
|
+
import { resolveChartScene, simulateDisplayView } from "@photivra/engine";
|
|
468
|
+
|
|
469
|
+
const chart = resolveChartScene({ scene: { preset: "siemens-star" }, aspect: 2 / 3 });
|
|
470
|
+
const label = chart.materials.length; // one extra material: a printed label in the lower left
|
|
471
|
+
const view = await simulateDisplayView({
|
|
472
|
+
scene: { kind: "material-function", sceneId: "star-with-label",
|
|
473
|
+
materials: [...chart.materials.map((m) => (m.kind === "neutral" ? m.reflectance : 0.5)), 0.05],
|
|
474
|
+
materialAt: (p) => (p.u < -0.6 && p.v < -0.5 ? label : chart.materialAt(p.u, p.v)) },
|
|
475
|
+
output: { width: 600, height: 400 }
|
|
476
|
+
}, { width: 300, height: 200 });
|
|
477
|
+
console.log(view.diagnostics.sceneLookups);
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
The display view's expected values match the reflectance-function form to rounding, because they are the same
|
|
481
|
+
quantities summed in another order. Measured on the app's chart with a maker label (a 24 MP frame, the 2028 × 1352 view's
|
|
482
|
+
centre strip, one thread), the material form took 1.15 s instead of 3.2 s with the defaults, and 3.0 s instead of 111 s
|
|
483
|
+
with diffraction, the four-spot filter and AHD.
|
|
484
|
+
|
|
443
485
|
## White balance
|
|
444
486
|
|
|
445
487
|
| Value | Meaning |
|
|
@@ -686,6 +728,14 @@ The optics options feed only what the environment site contract accepts:
|
|
|
686
728
|
lit circular pupil (scalar Fraunhofer diffraction), aberration-free and in focus, with pupil diameter
|
|
687
729
|
f/N and the thin-lens image distance (working f-number N·v/f). Each kernel holds the PSF energy of
|
|
688
730
|
site-sized cells, one kernel per spectral node.
|
|
731
|
+
- Sources: the Airy pattern is the textbook Fraunhofer diffraction pattern of a circular aperture
|
|
732
|
+
(Born & Wolf, *Principles of Optics*, 7th ed., §8.5.2; Goodman, *Introduction to Fourier Optics*,
|
|
733
|
+
4th ed., §4.4). The kernels come from the pupil's incoherent optical transfer function (Goodman,
|
|
734
|
+
4th ed., §7.3; MIT OpenCourseWare 2.71, lectures 21–22, free). The first dark ring is at
|
|
735
|
+
1.22·λ·N_w (j₁,₁/π with j₁,₁ = 3.8317, the first zero of J₁), so the Airy disk diameter is
|
|
736
|
+
2.44·λ·N_w, and the energy inside radius r is 1 − J₀(x)² − J₁(x)², x = π·r/(λ·N_w) (Wyant &
|
|
737
|
+
Creath 1992, §II, eqs. 1–4, free). The Bessel-function relations and expansions come from the
|
|
738
|
+
NIST DLMF (free). Full references are under [Sources](#sources).
|
|
689
739
|
- The kernel window reaches the third dark ring at the longest node by default (2, 4 and 7 sites at
|
|
690
740
|
f/4, f/8 and f/16 with 6 µm pixels); set `kernelRadiusSamples` (0–32) to change it. The Airy tail
|
|
691
741
|
outside the window (2–6 % of the energy with the default) is listed per wavelength in the
|
|
@@ -927,6 +977,8 @@ Ratios below about 1.1× are within the run-to-run spread (the three default run
|
|
|
927
977
|
(anti-aliasing spots, pupil rays, PSF taps, temporal samples) multiply the run time by about the same
|
|
928
978
|
factor, and output-side options cost almost nothing.
|
|
929
979
|
|
|
980
|
+
The cost of `simulateDisplayView` is in [Display view](#display-view).
|
|
981
|
+
|
|
930
982
|
### Measuring cost on the user's device
|
|
931
983
|
|
|
932
984
|
From 2.1 every result says what ran and how long each stage took, so an application can decide on the
|
|
@@ -1045,10 +1097,16 @@ How it stays identical:
|
|
|
1045
1097
|
own edges the region raster ends exactly where the whole frame's does, so `borderHandling: "mirror"`
|
|
1046
1098
|
mirrors at the same place. AHD reads at most 5 sites away, so its edge-directed choices see the same
|
|
1047
1099
|
neighbourhood.
|
|
1048
|
-
- **Diffraction:** the object-space convolution
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1100
|
+
- **Diffraction (2.6: region-limited):** the object-space convolution covers only the scene lattice the
|
|
1101
|
+
region's native sites query: the native rectangle plus the anti-aliasing and box-aperture offsets on
|
|
1102
|
+
each side, in bands of at most 32 native rows, with the kernel radius of real scene radiance around it.
|
|
1103
|
+
Each kept value is the same gather as in the whole render (the same kernel, the same taps in the same
|
|
1104
|
+
order, and every tap inside the grid), so the values are bit-identical while the work follows the
|
|
1105
|
+
region plus its halo instead of the frame width. In 2.3–2.5 the bands spanned the whole frame width, so
|
|
1106
|
+
a small crop of a wide frame paid for the full width and k tile columns repeated it k times. The noise
|
|
1107
|
+
needs no frame-wide state: each site's stream is seeded by a hash of the capture seed and its absolute
|
|
1108
|
+
native index (`seedSchedule: "murmur3-capture-seed-native-index-stream-v2"`), so it is counter-based,
|
|
1109
|
+
not a position in one frame-wide stream; the demosaic and the filters read only the margin above.
|
|
1052
1110
|
|
|
1053
1111
|
Measured on one core (Node 24, 4-vCPU Intel Xeon 2.80 GHz, built `dist`, one run each): a 256 × 256
|
|
1054
1112
|
slanted edge took 8.45 s whole, 9.03 s as four 256 × 64 strips and 8.44 s as four 128 × 128 tiles run
|
|
@@ -1056,6 +1114,25 @@ one after another; with `lens.diffraction` 10.2 s whole and 12.9 s as four tiles
|
|
|
1056
1114
|
convolution bands). A 64 × 64 crop of a 4000 × 3000 frame took 0.63 s, the same as a plain 64 × 64
|
|
1057
1115
|
capture. Tiles in parallel workers divide the wall time by the number of cores.
|
|
1058
1116
|
|
|
1117
|
+
**A 100 % crop with the slow options (2.6).** A 192 × 128 region at the centre of a 6000 × 4000 frame at 6 µm,
|
|
1118
|
+
Siemens star, f/16, `output.maximumPixels: NATIVE_RAW_LIMITS.ceilingPixels` (one fresh Node 24.21.0 process per
|
|
1119
|
+
row, 4-vCPU Intel Xeon 2.80 GHz, built `dist`, one run each; `diagnostics.timingsMs`):
|
|
1120
|
+
|
|
1121
|
+
| Options | 2.5.0 total (convolution) | 2.6 total (convolution) | Peak resident memory, 2.5.0 → 2.6 |
|
|
1122
|
+
| --- | --- | --- | --- |
|
|
1123
|
+
| defaults | 3.6 s (—) | 3.6 s (—) | 320 → 304 MB |
|
|
1124
|
+
| diffraction, four-spot filter, AHD | 60.8 s (50.2 s) | 11.8 s (1.7 s) | 556 → 379 MB |
|
|
1125
|
+
| diffraction, four-spot filter, AHD, box 2 × 2 | 228.7 s (195.8 s) | 39.6 s (6.5 s) | 1 285 → 545 MB |
|
|
1126
|
+
|
|
1127
|
+
On a GitHub-hosted `ubuntu-latest` runner (4 vCPU AMD EPYC 7763, Node 24, one run each) the same rows took 2.7 → 2.6 s,
|
|
1128
|
+
44.1 → 8.3 s and 170.9 → 27.7 s; from 2.6 the box row takes 19.4 s, because the engine no longer re-parses the fixed parts of its own chart scenes' results (caller scenes keep the full validation). The samples are byte-identical between the two versions. What remains is the per-site work, which grows with the
|
|
1129
|
+
scene queries per site (20 spectral nodes × the filter spots × the box nodes: 80 per site with the filter, 320
|
|
1130
|
+
with the filter and the 2 × 2 box) and with the region's sites, not with the frame.
|
|
1131
|
+
|
|
1132
|
+
`output.maximumPixels` is still needed for a region of a frame wider or taller than 4094 pixels: the request,
|
|
1133
|
+
plan and noise seeds are those of the whole frame, and the whole frame's native raster is checked against the
|
|
1134
|
+
limit. The allocations follow the region, so the opt-in costs no memory for the rest of the frame.
|
|
1135
|
+
|
|
1059
1136
|
`result.image` holds the region, `result.raw` and `result.plan` describe the region's native sites (the
|
|
1060
1137
|
plan's active area is the region; `result.region.nativeRect` gives them in the whole frame), and
|
|
1061
1138
|
`result.region` reports the rectangle and the frame. `diagnostics.options` lists `output.region`.
|
|
@@ -1117,6 +1194,101 @@ const [capture, reference] = await Promise.all([simulateCapture(request), simula
|
|
|
1117
1194
|
console.log(reference.kind, reference.label, capture.image.samples.length === reference.image.samples.length);
|
|
1118
1195
|
```
|
|
1119
1196
|
|
|
1197
|
+
## Display view
|
|
1198
|
+
|
|
1199
|
+
`simulateDisplayView(options, view)` returns what the real full-resolution capture of `options` would look like
|
|
1200
|
+
resized to `view.width` × `view.height`, at a cost that follows the display pixels. Pass the same request you use
|
|
1201
|
+
for the export, with the real sensor's pixel count and pitch:
|
|
1202
|
+
|
|
1203
|
+
```ts
|
|
1204
|
+
import { NATIVE_RAW_LIMITS, simulateCapture, simulateDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
|
|
1205
|
+
|
|
1206
|
+
const request: SimulateCaptureOptions = { scene: { preset: "dead-leaves" }, lens: { prime: 35, fNumber: 2.8 }, subjectDistanceM: 3,
|
|
1207
|
+
sensor: { pixelPitchMicrometers: 6 }, output: { width: 6000, height: 4000, maximumPixels: NATIVE_RAW_LIMITS.ceilingPixels } };
|
|
1208
|
+
const preview = await simulateDisplayView(request, { width: 384, height: 256 }); // on screen
|
|
1209
|
+
console.log(preview.kind, preview.label, preview.diagnostics.timingsMs.view);
|
|
1210
|
+
// The export stays a real capture: simulateCapture(request), or tiles of it with output.region.
|
|
1211
|
+
const crop = await simulateCapture({ ...request, output: { ...request.output, region: { x: 2968, y: 1968, width: 64, height: 64 } } });
|
|
1212
|
+
console.log(crop.image.width);
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
**What each display pixel is.** The area-weighted mean, in linear light, of the real output pixels its rectangle covers
|
|
1216
|
+
(6000 ÷ 384 = 15.625 real pixels per axis here), sRGB-encoded and rounded: area (box) resampling, the standard
|
|
1217
|
+
reduction filter. Lens diffraction, the anti-aliasing filter and the pixel aperture act at the real 6 µm pitch; the
|
|
1218
|
+
colour mosaic, demosaic and development are those of the real pixels; the noise is that of the average of the real
|
|
1219
|
+
pixels, demosaic correlation included; exposure, white balance, tone curve and framing are the capture's (the same code
|
|
1220
|
+
resolves them).
|
|
1221
|
+
|
|
1222
|
+
**How it is computed** (details and error analysis in [DISPLAY_VIEW.md](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md)):
|
|
1223
|
+
|
|
1224
|
+
- The expected value of each display pixel is a stratified Monte Carlo average over K × K real pixels of its rectangle
|
|
1225
|
+
(`view.samplesPerAxis`, default 16), seeded and unbiased. For each sampled pixel the expected electrons of the real
|
|
1226
|
+
sites its demosaic reads are exact, and the clips and tone curve act on the pixel's own noise distribution. A pilot
|
|
1227
|
+
grid gives uniform areas K/4 samples per axis; their error is zero anyway.
|
|
1228
|
+
- The noise is one correlated normal draw per display pixel with the covariance of the area mean of the real pixels'
|
|
1229
|
+
independent site noises (shot, dark, read, ADC), including the demosaic's correlation.
|
|
1230
|
+
- AHD is represented by its two linear candidates; where the noise is large enough for its homogeneity choice to shift
|
|
1231
|
+
the colour, a control-variate correction runs the engine's AHD steps on drawn noisy neighbourhoods.
|
|
1232
|
+
|
|
1233
|
+
Validated against real full-resolution captures resized in linear light: three sensors (12 MP phone, 24 MP full frame,
|
|
1234
|
+
100 MP medium format), five charts, f/2.8–f/16, ISO 100–6400, 96–800 display pixels wide, Speed and Quality options (see
|
|
1235
|
+
[Validation results](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#validation-results)).
|
|
1236
|
+
|
|
1237
|
+
**Cost.** It follows the detailed display pixels × `samplesPerAxis`², not the real pixel count. Measured against the
|
|
1238
|
+
whole-frame view of the same request (Siemens star, f/8, ISO 100, one run, Node 24.21.0 on a 4-vCPU 2.80 GHz Xeon;
|
|
1239
|
+
every row, and the times before the exact speed-ups, in [DISPLAY_VIEW.md](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#cost)):
|
|
1240
|
+
|
|
1241
|
+
| Sensor | Options | 96 wide | 384 wide | 800 wide |
|
|
1242
|
+
| --- | --- | --- | --- | --- |
|
|
1243
|
+
| 24 MP full frame, 6 µm | Speed (defaults) | 2.4 s (2.3×) | 10.6 s (0.92×) | 26.6 s (0.65×) |
|
|
1244
|
+
| 12 MP phone, 1.4 µm | Speed | 2.2 s (2.4×) | 11.7 s (0.92×) | 32.1 s (0.59×) |
|
|
1245
|
+
| 100 MP medium format, 3.76 µm | Speed | 2.7 s (2.3×) | 15.8 s (1.2×) | 32.6 s (0.66×) |
|
|
1246
|
+
| 24 MP full frame | Quality (diffraction, four-spot filter, AHD) | 5.2 s (1.6×) | 30.9 s (1.3×) | 84.3 s (0.76×) |
|
|
1247
|
+
| 12 MP phone | Quality | 49.7 s (20×) | 106 s (3.8×) | 272 s (2.2×) |
|
|
1248
|
+
| 100 MP medium format | Quality | 18.2 s (3.7×) | 49.7 s (1.8×) | 126 s (1.0×) |
|
|
1249
|
+
|
|
1250
|
+
Quality costs more on small pixels. The diffraction kernel spans several real sites, and at 1.4 µm even ISO 100 is
|
|
1251
|
+
noisy enough for the AHD correction to run on every sampled pixel; its correction states are kept for later views of
|
|
1252
|
+
the same sensor, exposure and development in the same JavaScript realm (a second 384-wide phone view of the same
|
|
1253
|
+
request took 73 s instead of 103 s). The results are byte-identical to those of the display view as first merged,
|
|
1254
|
+
which took 222 s, 450 s and 837 s on the phone.
|
|
1255
|
+
|
|
1256
|
+
**Options.** `view.width` and `view.height` (1 to the capture's output size), `view.samplesPerAxis` (1–32),
|
|
1257
|
+
`view.format` (`"srgb-8"` default or `"srgb-16"`) and `view.region` (a rectangle in display pixels, byte-identical to
|
|
1258
|
+
the same pixels of the whole view, for tiling across workers). The capture's file-format options (`format`,
|
|
1259
|
+
`colorEncoding`, `bitDepth`, `gamutMapping`, `jpegQuantizationStep`, `dng`, `metadata`) do not apply. The request must
|
|
1260
|
+
be one `simulateCapture` accepts (a frame wider or taller than 4094 pixels needs `output.maximumPixels`, although the
|
|
1261
|
+
view allocates nothing at full resolution).
|
|
1262
|
+
|
|
1263
|
+
**Many regions of one view (2.7).** Each `simulateDisplayView` call repeats the request's setup: the camera, lens,
|
|
1264
|
+
scene and illuminant, the channel responses, the diffraction kernels and the colour tables. That is about 0.25 s with
|
|
1265
|
+
the engine defaults and 0.7 s with the Quality options on a 24 MP request. To render one view in many regions, prepare
|
|
1266
|
+
the request once and render each region from it; each region then costs only its own pixels:
|
|
1267
|
+
|
|
1268
|
+
```ts
|
|
1269
|
+
import { prepareDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
|
|
1270
|
+
|
|
1271
|
+
const request: SimulateCaptureOptions = { scene: { preset: "siemens-star" }, output: { width: 600, height: 400 } };
|
|
1272
|
+
const prepared = await prepareDisplayView(request); // request errors are raised here
|
|
1273
|
+
const tile = await prepared.render({ width: 300, height: 200, region: { x: 120, y: 80, width: 32, height: 32 } },
|
|
1274
|
+
{ signal: AbortSignal.timeout(60_000) }); // cancels this render only
|
|
1275
|
+
console.log(tile.image.width, tile.diagnostics.timingsMs.view);
|
|
1276
|
+
```
|
|
1277
|
+
|
|
1278
|
+
`render(view)` returns what `simulateDisplayView(request, view)` returns, byte for byte (only `diagnostics.timingsMs`
|
|
1279
|
+
differ, and they cover the render only). Renders may run concurrently and share only read-only setup. The request's own
|
|
1280
|
+
`signal` applies to preparing; give each render its own. Keep one prepared view per request in each worker: measured on
|
|
1281
|
+
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).
|
|
1282
|
+
|
|
1283
|
+
**Not supported** (refused with an `InvalidConfigurationError`): `lens.pupilSampling`, `lens.projection`,
|
|
1284
|
+
`lens.distortion`, `lens.vignetting`, `lens.psf`, camera rotation, `output.whiteBalance: "auto"`, the luminance tone
|
|
1285
|
+
curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `onProgress` is not called;
|
|
1286
|
+
`signal` cancels.
|
|
1287
|
+
|
|
1288
|
+
The result is labelled `kind: "display-view"` with a `label` for the user interface; it has no RAW, plan or file, and
|
|
1289
|
+
the `view.*` assumptions state the definition, the estimator, the noise model, the demosaic treatment and which capture
|
|
1290
|
+
options were ignored. The whole-frame views of [Whole-frame views](#whole-frame-views-25) are unchanged.
|
|
1291
|
+
|
|
1120
1292
|
## Not available
|
|
1121
1293
|
|
|
1122
1294
|
The facade offers no atmospheric haze or polarizer (see [Optics](#optics)), flash, video, HDR brackets
|
|
@@ -1125,6 +1297,33 @@ provided, for rights reasons.
|
|
|
1125
1297
|
|
|
1126
1298
|
## Sources
|
|
1127
1299
|
|
|
1300
|
+
- Diffraction (`lens.diffraction`, `createDiffractionLimitedPsfProfile`): M. Born, E. Wolf,
|
|
1301
|
+
*Principles of Optics*, 7th (expanded) ed., Cambridge University Press, 1999,
|
|
1302
|
+
doi:10.1017/CBO9781139644181, §8.5.2 "The circular aperture" (Fraunhofer diffraction at a circular
|
|
1303
|
+
aperture: the Airy pattern and its dark rings); J. W. Goodman, *Introduction to Fourier Optics*,
|
|
1304
|
+
4th ed., W. H. Freeman, 2017, §4.4 (examples of Fraunhofer diffraction patterns, including the
|
|
1305
|
+
circular aperture) and §7.3 (frequency response for diffraction-limited incoherent imaging: the
|
|
1306
|
+
optical transfer function as the normalized pupil autocorrelation); J. C. Wyant, K. Creath, "Basic
|
|
1307
|
+
Wavefront Aberration Theory for Optical Metrology", *Applied Optics and Optical Engineering* XI,
|
|
1308
|
+
ch. 1, Academic Press, 1992, §II "Aberration-Free Image", eqs. 1–4, free author copy at
|
|
1309
|
+
<https://wp.optics.arizona.edu/jcwyant/wp-content/uploads/sites/13/2016/08/03-BasicAberrations_and_Optical_Testing.pdf>
|
|
1310
|
+
(the Airy irradiance at the effective f-number, the 2.44·λ·N Airy disk diameter and the encircled
|
|
1311
|
+
energy 1 − J₀² − J₁²); MIT OpenCourseWare 2.71 Optics (Spring 2009), lectures 21–22, free at
|
|
1312
|
+
<https://ocw.mit.edu/courses/2-71-optics-spring-2009/resources/mit2_71s09_lec22/> (the intensity PSF as the transform of the
|
|
1313
|
+
pupil autocorrelation and the circular-pupil MTF); F. W. J. Olver et al. (eds.), *NIST Digital
|
|
1314
|
+
Library of Mathematical Functions*, release 1.2.8 (2026-09-15), free at <https://dlmf.nist.gov/>,
|
|
1315
|
+
§10.6(i) (eqs. 10.6.2 and 10.6.3: J₀′ = −J₁ and J₁′ = J₀ − J₁/x, from which the encircled energy
|
|
1316
|
+
follows), eq. 10.17.3 and §10.17(iii) (Hankel's large-argument expansion of J₀ and J₁ and its error
|
|
1317
|
+
bound, used for cells much larger than the pattern) and eq. 10.9.2 (Bessel's integral, used as an
|
|
1318
|
+
independent test). The original analysis is G. B. Airy, "On the Diffraction of an Object-glass with
|
|
1319
|
+
Circular Aperture", *Transactions of the Cambridge Philosophical Society* 5, 1835, pp. 283–291. The
|
|
1320
|
+
engine's implementation and its tests are its own (see [PSF foundation](https://github.com/Photivra/photivra/blob/main/docs/PSF_FOUNDATION.md#diffraction-limited-sampled-profile-21)).
|
|
1321
|
+
- Print planning (`calculatePrintPlan`, `calculatePrintSizeLimit`): Z. Alomar, K. D. Bojikian, "Visual Acuity
|
|
1322
|
+
Testing in Adults", EyeWiki, American Academy of Ophthalmology, last edited June 16, 2026, free to read at
|
|
1323
|
+
<https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults> (the minimum angle of resolution of an ideal eye under optimal
|
|
1324
|
+
conditions is about 1 minute of arc; 20/20 letters are built from 1-arcmin elements). One arcminute per pixel is normal
|
|
1325
|
+
(20/20) acuity under optimal conditions and an approximate rule of thumb: some viewers resolve finer detail and many
|
|
1326
|
+
coarser. The planner uses the criterion the caller declares.
|
|
1128
1327
|
- R. Palum, "Anti-Aliasing Filter Analysis for Digital Cameras", Proc. IS&T International Congress of
|
|
1129
1328
|
Imaging Science (ICIS '06), 2006, free at
|
|
1130
1329
|
<https://www.imaging.org/common/uploaded%20files/pdfs/Papers/2006/ICIS-0-736/33683.pdf>
|
package/docs/USAGE.md
CHANGED
|
@@ -7501,6 +7501,21 @@ The chart presets are also available on their own:
|
|
|
7501
7501
|
- `createPlanarSceneRadianceProvider()` turns a chart into an `evaluateRadiance` callback for the native
|
|
7502
7502
|
environment RAW contract.
|
|
7503
7503
|
|
|
7504
|
+
### Display view of a capture
|
|
7505
|
+
|
|
7506
|
+
`simulateDisplayView(options, view)` takes a capture request and a display size and returns the real
|
|
7507
|
+
full-resolution capture resized to that size (area average in linear light), estimated at a cost that follows the
|
|
7508
|
+
display pixels. It is a preview (`kind: "display-view"`), never a capture; see [Display view](QUICK_START.md#display-view)
|
|
7509
|
+
and the design and validation in [DISPLAY_VIEW.md](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md).
|
|
7510
|
+
|
|
7511
|
+
```ts
|
|
7512
|
+
import { simulateDisplayView } from "@photivra/engine";
|
|
7513
|
+
|
|
7514
|
+
const view = await simulateDisplayView({ scene: { preset: "slanted-edge" }, sensor: { pixelPitchMicrometers: 3 }, output: { width: 600, height: 400 } },
|
|
7515
|
+
{ width: 60, height: 40 });
|
|
7516
|
+
console.log(view.kind, view.view.scaleX, view.assumptions.find((a) => a.id === "view.definition")?.value);
|
|
7517
|
+
```
|
|
7518
|
+
|
|
7504
7519
|
### Facade 2.0 options: output, demosaic, optics and motion
|
|
7505
7520
|
|
|
7506
7521
|
Each 2.0 option is opt-in and uses an existing public contract. Without them, the facade's output is
|
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.7.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.7.0",
|
|
3
|
+
"engineApiVersion": "2.7.0",
|
|
4
4
|
"exports": [
|
|
5
5
|
{
|
|
6
6
|
"name": "AbsoluteUncertaintyEstimate",
|
|
@@ -1417,6 +1417,16 @@
|
|
|
1417
1417
|
"source": "src/sensor/radiometry-readiness.ts",
|
|
1418
1418
|
"kind": "type"
|
|
1419
1419
|
},
|
|
1420
|
+
{
|
|
1421
|
+
"name": "CALLER_MATERIAL_SCENE_MAX_MATERIALS",
|
|
1422
|
+
"source": "src/facade/scene-radiance.ts",
|
|
1423
|
+
"kind": "runtime"
|
|
1424
|
+
},
|
|
1425
|
+
{
|
|
1426
|
+
"name": "CallerMaterialScene",
|
|
1427
|
+
"source": "src/facade/scene-radiance.ts",
|
|
1428
|
+
"kind": "type"
|
|
1429
|
+
},
|
|
1420
1430
|
{
|
|
1421
1431
|
"name": "CallerRadianceScene",
|
|
1422
1432
|
"source": "src/facade/scene-radiance.ts",
|
|
@@ -2582,6 +2592,11 @@
|
|
|
2582
2592
|
"source": "src/core/spectral-composition.ts",
|
|
2583
2593
|
"kind": "type"
|
|
2584
2594
|
},
|
|
2595
|
+
{
|
|
2596
|
+
"name": "DisplayViewResult",
|
|
2597
|
+
"source": "src/facade/simulate-capture.ts",
|
|
2598
|
+
"kind": "type"
|
|
2599
|
+
},
|
|
2585
2600
|
{
|
|
2586
2601
|
"name": "DistributeIntegratedQuantityAcrossDiscreteLinesInput",
|
|
2587
2602
|
"source": "src/core/spectral-composition.ts",
|
|
@@ -5367,6 +5382,16 @@
|
|
|
5367
5382
|
"source": "src/color/color-matching-functions.ts",
|
|
5368
5383
|
"kind": "type"
|
|
5369
5384
|
},
|
|
5385
|
+
{
|
|
5386
|
+
"name": "PreparedDisplayView",
|
|
5387
|
+
"source": "src/facade/simulate-display-view.ts",
|
|
5388
|
+
"kind": "type"
|
|
5389
|
+
},
|
|
5390
|
+
{
|
|
5391
|
+
"name": "PreparedDisplayViewRenderOptions",
|
|
5392
|
+
"source": "src/facade/simulate-display-view.ts",
|
|
5393
|
+
"kind": "type"
|
|
5394
|
+
},
|
|
5370
5395
|
{
|
|
5371
5396
|
"name": "PreparedGeometricMapping",
|
|
5372
5397
|
"source": "src/output/geometric-transforms.ts",
|
|
@@ -5377,6 +5402,11 @@
|
|
|
5377
5402
|
"source": "src/composition/image-formation-plan.ts",
|
|
5378
5403
|
"kind": "type"
|
|
5379
5404
|
},
|
|
5405
|
+
{
|
|
5406
|
+
"name": "prepareDisplayView",
|
|
5407
|
+
"source": "src/facade/simulate-display-view.ts",
|
|
5408
|
+
"kind": "runtime"
|
|
5409
|
+
},
|
|
5380
5410
|
{
|
|
5381
5411
|
"name": "PreparedRgbGamutMapping",
|
|
5382
5412
|
"source": "src/color/gamut-mapping.ts",
|
|
@@ -6802,6 +6832,11 @@
|
|
|
6802
6832
|
"source": "src/schema/illumination.ts",
|
|
6803
6833
|
"kind": "type"
|
|
6804
6834
|
},
|
|
6835
|
+
{
|
|
6836
|
+
"name": "SceneMaterialPoint",
|
|
6837
|
+
"source": "src/facade/scene-radiance.ts",
|
|
6838
|
+
"kind": "type"
|
|
6839
|
+
},
|
|
6805
6840
|
{
|
|
6806
6841
|
"name": "SceneMaterialResponseDefinition",
|
|
6807
6842
|
"source": "src/schema/scene-radiance.ts",
|
|
@@ -7907,6 +7942,11 @@
|
|
|
7907
7942
|
"source": "src/facade/simulate-capture.ts",
|
|
7908
7943
|
"kind": "runtime"
|
|
7909
7944
|
},
|
|
7945
|
+
{
|
|
7946
|
+
"name": "SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS",
|
|
7947
|
+
"source": "src/facade/simulate-capture.ts",
|
|
7948
|
+
"kind": "runtime"
|
|
7949
|
+
},
|
|
7910
7950
|
{
|
|
7911
7951
|
"name": "simulateCapture",
|
|
7912
7952
|
"source": "src/facade/simulate-capture.ts",
|
|
@@ -8002,6 +8042,16 @@
|
|
|
8002
8042
|
"source": "src/capture/simulated-capture.ts",
|
|
8003
8043
|
"kind": "type"
|
|
8004
8044
|
},
|
|
8045
|
+
{
|
|
8046
|
+
"name": "simulateDisplayView",
|
|
8047
|
+
"source": "src/facade/simulate-display-view.ts",
|
|
8048
|
+
"kind": "runtime"
|
|
8049
|
+
},
|
|
8050
|
+
{
|
|
8051
|
+
"name": "SimulateDisplayViewOptions",
|
|
8052
|
+
"source": "src/facade/simulate-capture.ts",
|
|
8053
|
+
"kind": "type"
|
|
8054
|
+
},
|
|
8005
8055
|
{
|
|
8006
8056
|
"name": "simulateEnvironmentSensorRawFrame",
|
|
8007
8057
|
"source": "src/capture/environment-raw-producer.ts",
|