@photivra/engine 2.4.1 → 2.6.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 +53 -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-scenes.d.ts +9 -0
- package/dist/facade/chart-scenes.js +17 -4
- 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 +208 -0
- package/dist/facade/display-view.js +1461 -0
- package/dist/facade/display-view.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +7 -0
- package/dist/facade/scene-radiance.js +18 -5
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +229 -5
- package/dist/facade/simulate-capture.js +119 -35
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +21 -0
- package/dist/facade/simulate-display-view.js +168 -0
- package/dist/facade/simulate-display-view.js.map +1 -0
- package/dist/facade/subject-framing.d.ts +78 -0
- package/dist/facade/subject-framing.js +69 -20
- 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 +59 -3
- package/dist/optics/diffraction-psf-profile.js +147 -5
- 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 +16 -1
- package/docs/QUICK_START.md +245 -9
- package/docs/USAGE.md +15 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +37 -2
- package/docs/api/facade-simulate-capture.md +70 -2
- package/docs/api/facade-simulate-display-view.md +18 -0
- package/docs/api/facade-subject-framing.md +83 -0
- package/docs/api/optics-diffraction-psf-profile.md +8 -1
- 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.6.0 / root contract 2.6.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) |
|
|
@@ -1179,11 +1180,13 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1179
1180
|
|
|
1180
1181
|
| Export | Kind |
|
|
1181
1182
|
| --- | --- |
|
|
1183
|
+
| [`DisplayViewResult`](api/facade-simulate-capture.md#displayviewresult) | Type |
|
|
1182
1184
|
| [`IdealReferenceResult`](api/facade-simulate-capture.md#idealreferenceresult) | Type |
|
|
1183
1185
|
| [`SIMULATE_CAPTURE_BOX_APERTURE_DEFAULT_SAMPLES`](api/facade-simulate-capture.md#simulate_capture_box_aperture_default_samples) | Runtime |
|
|
1184
1186
|
| [`SIMULATE_CAPTURE_COST_OPTIONS`](api/facade-simulate-capture.md#simulate_capture_cost_options) | Runtime |
|
|
1185
1187
|
| [`SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX`](api/facade-simulate-capture.md#simulate_capture_default_illuminance_lux) | Runtime |
|
|
1186
1188
|
| [`SIMULATE_CAPTURE_FACADE_VERSION`](api/facade-simulate-capture.md#simulate_capture_facade_version) | Runtime |
|
|
1189
|
+
| [`SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS`](api/facade-simulate-capture.md#simulate_display_view_default_samples_per_axis) | Runtime |
|
|
1187
1190
|
| [`simulateCapture`](api/facade-simulate-capture.md#simulatecapture) | Runtime |
|
|
1188
1191
|
| [`SimulateCaptureAssumption`](api/facade-simulate-capture.md#simulatecaptureassumption) | Type |
|
|
1189
1192
|
| [`SimulateCaptureBorderHandling`](api/facade-simulate-capture.md#simulatecaptureborderhandling) | Type |
|
|
@@ -1200,16 +1203,28 @@ Start here to find a function by task. [Using Photivra from an agent](AGENT_GUID
|
|
|
1200
1203
|
| [`SimulateCaptureResult`](api/facade-simulate-capture.md#simulatecaptureresult) | Type |
|
|
1201
1204
|
| [`SimulateCaptureSensorAntiAliasing`](api/facade-simulate-capture.md#simulatecapturesensorantialiasing) | Type |
|
|
1202
1205
|
| [`SimulateCaptureWhiteBalance`](api/facade-simulate-capture.md#simulatecapturewhitebalance) | Type |
|
|
1206
|
+
| [`SimulateDisplayViewOptions`](api/facade-simulate-capture.md#simulatedisplayviewoptions) | Type |
|
|
1203
1207
|
| [`simulateIdealReference`](api/facade-simulate-capture.md#simulateidealreference) | Runtime |
|
|
1204
1208
|
|
|
1209
|
+
## facade/simulate-display-view.ts
|
|
1210
|
+
|
|
1211
|
+
[Detailed contracts](api/facade-simulate-display-view.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/simulate-display-view.ts)
|
|
1212
|
+
|
|
1213
|
+
| Export | Kind |
|
|
1214
|
+
| --- | --- |
|
|
1215
|
+
| [`simulateDisplayView`](api/facade-simulate-display-view.md#simulatedisplayview) | Runtime |
|
|
1216
|
+
|
|
1205
1217
|
## facade/subject-framing.ts
|
|
1206
1218
|
|
|
1207
1219
|
[Detailed contracts](api/facade-subject-framing.md) · [Source](https://github.com/Photivra/photivra/blob/main/src/facade/subject-framing.ts)
|
|
1208
1220
|
|
|
1209
1221
|
| Export | Kind |
|
|
1210
1222
|
| --- | --- |
|
|
1223
|
+
| [`calculateChartFraming`](api/facade-subject-framing.md#calculatechartframing) | Runtime |
|
|
1224
|
+
| [`CalculateChartFramingInput`](api/facade-subject-framing.md#calculatechartframinginput) | Type |
|
|
1211
1225
|
| [`calculateSubjectDistanceForFraming`](api/facade-subject-framing.md#calculatesubjectdistanceforframing) | Runtime |
|
|
1212
1226
|
| [`CalculateSubjectDistanceForFramingInput`](api/facade-subject-framing.md#calculatesubjectdistanceforframinginput) | Type |
|
|
1227
|
+
| [`ChartFraming`](api/facade-subject-framing.md#chartframing) | Type |
|
|
1213
1228
|
| [`SubjectFraming`](api/facade-subject-framing.md#subjectframing) | Type |
|
|
1214
1229
|
| [`SubjectFramingFit`](api/facade-subject-framing.md#subjectframingfit) | Type |
|
|
1215
1230
|
|
package/docs/QUICK_START.md
CHANGED
|
@@ -98,7 +98,7 @@ modelling choices (such as "no lens PSF") as `kind: "model"`.
|
|
|
98
98
|
| `output.highlightMode` | `"clip"` | Or `"reconstruct"`. |
|
|
99
99
|
| `output.whiteBalance` | `"scene-illuminant"` | Also `"auto"`, `"none"`, `{ kelvin, tint? }` and `{ channelGains }`. |
|
|
100
100
|
| `output.jpegQuantizationStep` | 2 | JPEG only: constant step 1–255, not a manufacturer quality scale. |
|
|
101
|
-
| `sensor.pixelPitchMicrometers` | 6 | 0.5–100. 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)). |
|
|
101
|
+
| `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)). |
|
|
102
102
|
| `sensor.spectralStepNanometers` | 20 | Spectral quadrature over 380–780 nm. |
|
|
103
103
|
| `sensor.temporalSampleCount` | 1, or 8 when the camera rotates | Midpoints across each site's exposure window. |
|
|
104
104
|
| `sensor.noiseSeed` | 1 | Output is deterministic for a given seed. |
|
|
@@ -151,7 +151,8 @@ console.log(a3.scene.chart?.frameWidthFraction, a3.scene.chart?.magnification, a
|
|
|
151
151
|
The simulated sensor has real pixels of `sensor.pixelPitchMicrometers`. A 64 × 64 capture with 6 µm
|
|
152
152
|
pixels is therefore a 0.38 mm crop of a large sensor, not a downscaled full frame. This keeps the
|
|
153
153
|
signal per pixel, and so the noise and the ISO behaviour, physically consistent with the tier's
|
|
154
|
-
sensor values.
|
|
154
|
+
sensor values. To see a whole frame at a small output size instead, raise the pitch (see
|
|
155
|
+
[Whole-frame views](#whole-frame-views-25)).
|
|
155
156
|
|
|
156
157
|
### Generic prime lenses (2.3)
|
|
157
158
|
|
|
@@ -162,7 +163,8 @@ real products, and carry no brand:
|
|
|
162
163
|
|
|
163
164
|
- **Focal length** = equivalent ÷ the diagonal crop factor of the sensor (`calculateEquivalentFocalLength35Mm`,
|
|
164
165
|
36 × 24 mm reference). In `simulateCapture` the sensor is the simulated frame, output width × height ×
|
|
165
|
-
pixel pitch: to simulate a whole full-frame sensor at 768 × 512, set the pitch to 36 mm / 768 = 46.875 µm
|
|
166
|
+
pixel pitch: to simulate a whole full-frame sensor at 768 × 512, set the pitch to 36 mm / 768 = 46.875 µm (or 750 µm at
|
|
167
|
+
48 × 32, see [Whole-frame views](#whole-frame-views-25)).
|
|
166
168
|
- **Aperture range:** widest f/2.8 (10–16 mm), f/1.8 (20–28 mm), f/1.4 (35–85 mm), f/2 (105–135 mm),
|
|
167
169
|
f/2.8 (200–300 mm), f/4 (400–600 mm), f/5.6 (800 mm), f/8 (1200 mm); narrowest f/16 up to 16 mm, f/22 up
|
|
168
170
|
to 135 mm, f/32 beyond. Any f-number inside the range is accepted; `fNumbers` lists the marked
|
|
@@ -190,7 +192,8 @@ cropped on its other side). The result reports the magnification, image distance
|
|
|
190
192
|
`coverage` (the fraction of the frame width and height the chart covers), the generic prime of that
|
|
191
193
|
equivalent focal length if there is one, the prime family's minimum focus distance at that focal length
|
|
192
194
|
and `belowMinimumFocus`, plus assumptions in the same form as `simulateCapture`. It is the same framing
|
|
193
|
-
`simulateCapture` uses, so `chartSize` at the returned distance gives `frameWidthFraction` 1 (tested).
|
|
195
|
+
`simulateCapture` uses, so `chartSize` at the returned distance gives `frameWidthFraction` 1 (tested). For the coverage at any other distance, see
|
|
196
|
+
[Chart coverage at any distance](#chart-coverage-at-any-distance-25).
|
|
194
197
|
|
|
195
198
|
An A3 chart (0.42 × 0.28 m) on a full-frame sensor: 0.127 m at 10 mm, 0.633 m at 50 mm, 2.53 m at
|
|
196
199
|
200 mm and 15.2 m at 1200 mm (magnification 0.0857, minimum focus 0.10, 0.38, 1.53 and 9.2 m). On a
|
|
@@ -208,6 +211,101 @@ const capture = await simulateCapture({ scene: { preset: "siemens-star" }, lens:
|
|
|
208
211
|
console.log(capture.scene.chart?.frameWidthFraction);
|
|
209
212
|
```
|
|
210
213
|
|
|
214
|
+
### Chart coverage at any distance (2.5)
|
|
215
|
+
|
|
216
|
+
`calculateChartFraming({ sensor, focalLengthMm | equivalentFocalLengthMm, chart, subjectDistanceM, focusDistanceM? })`
|
|
217
|
+
answers the inverse question: how much of the frame does a chart of physical size `chart` (`{ widthM, heightM? }`, as
|
|
218
|
+
for `chartSize`) cover at a given distance? It uses the facade's own thin-lens framing: image distance
|
|
219
|
+
v = f·s/(s − f) at the focus distance s (default: the subject distance), magnification m = v/d on the chart plane at
|
|
220
|
+
d, and a frame that spans sensor side ÷ m. The result reports the magnification, image distance, framed plane size,
|
|
221
|
+
`coverage` (`frameWidthFraction` and `frameHeightFraction`; above 1 when the chart is larger than the frame and
|
|
222
|
+
cropped), the chart's image size in millimetres, the generic prime of that equivalent focal length if there is one,
|
|
223
|
+
the prime family's minimum focus distance and `belowMinimumFocus` (the subject or focus distance is closer than it,
|
|
224
|
+
which `simulateCapture` with `lens.prime` refuses), plus assumptions.
|
|
225
|
+
|
|
226
|
+
An A3 chart (0.42 × 0.28 m) with the 50 mm full-frame prime covers 130 % of the frame width at 0.5 m (cropped),
|
|
227
|
+
61.4 % at 1 m, 29.9 % at 2 m and 11.8 % at 5 m (magnification 0.111, 0.0526, 0.0256 and 0.0101).
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { calculateChartFraming } from "@photivra/engine";
|
|
231
|
+
|
|
232
|
+
const coverage = calculateChartFraming({ sensor: { widthMm: 36, heightMm: 24 }, equivalentFocalLengthMm: 50, chart: { widthM: 0.42, heightM: 0.28 }, subjectDistanceM: 2 });
|
|
233
|
+
console.log(coverage.coverage.frameWidthFraction, coverage.magnification, coverage.belowMinimumFocus);
|
|
234
|
+
```
|
|
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
|
+
|
|
246
|
+
Tests check that at `calculateSubjectDistanceForFraming`'s distance the coverage is 1 on the fitted side (every fit,
|
|
247
|
+
three sensor shapes, four primes), and that at any distance it equals `result.scene.chart` from `simulateCapture` with
|
|
248
|
+
`chartSize` (magnification, both coverage fractions, the chart's image size and the framed size, within 1e-12
|
|
249
|
+
relative), including a separate `lens.focusDistanceM`.
|
|
250
|
+
|
|
251
|
+
### Whole-frame views (2.5)
|
|
252
|
+
|
|
253
|
+
To render the whole sensor frame at a small output size, set the pitch to sensor width ÷ output width:
|
|
254
|
+
36 mm at 48 pixels is 750 µm, 44 mm at 48 pixels 917 µm. From 2.5 the pitch may be up to 1000 µm. The
|
|
255
|
+
frame is then the real sensor's size, so `lens.prime` (its crop factor and minimum focus),
|
|
256
|
+
`calculateSubjectDistanceForFraming` and `chartSize` all match that sensor, and the image is a shrunk view of
|
|
257
|
+
the whole frame. No new option is needed: the facade has always simulated a sensor `width × pitch` wide.
|
|
258
|
+
|
|
259
|
+
```ts
|
|
260
|
+
import { simulateCapture } from "@photivra/engine";
|
|
261
|
+
|
|
262
|
+
const view = await simulateCapture({ scene: { preset: "dead-leaves" }, lens: { prime: 35, fNumber: 2.8 }, subjectDistanceM: 3,
|
|
263
|
+
sensor: { pixelPitchMicrometers: 36000 / 48, pixelAperture: "box" }, output: { width: 48, height: 32 } });
|
|
264
|
+
console.log(view.assumptions.find((a) => a.id === "sensor.wholeFrameView")?.value); // { pixelPitchMicrometers: 750, frameWidthMm: 36, frameHeightMm: 24 }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Above 100 µm (larger than any camera photosite) the assumption `sensor.wholeFrameView` records the frame size.
|
|
268
|
+
What changes and what does not:
|
|
269
|
+
|
|
270
|
+
- **Brightness is the same as at any pitch.** The ISO area normalization (below) multiplies the conversion gain
|
|
271
|
+
and saturation by the area ratio, up to 27 778 at 1000 µm, so the RAW codes and the image are those of a small
|
|
272
|
+
pitch. Tested at 150–1000 µm against 6 µm: every CFA channel's mean RAW code within half a code (plus the 6 µm
|
|
273
|
+
reference's own noise), the 8-bit sRGB mean within one code, and a 64× overexposure clipping at the same white
|
|
274
|
+
code. Charge counts reach 1.7 × 10⁹ e⁻; the Poisson sampler's normal branch above 10⁹ keeps variance/mean at 1.
|
|
275
|
+
- **Noise is that of a binned, downsized view, not the real sensor's per-pixel noise.** Each output pixel
|
|
276
|
+
collects the charge of its whole square, so its shot noise equals that of all the real photosites in the
|
|
277
|
+
square summed (Poisson counts add) or averaged, and its SNR √N_e grows in proportion to the pitch. On an 18 %
|
|
278
|
+
card at f/8, 1/125 s, ISO 100 a 917 µm pixel collects about 1.6 × 10⁸ e⁻ (SNR about 12 700): under a fifth of a
|
|
279
|
+
RAW code, so the image looks noise-free, as a downsized photograph does. Read noise and dark current are applied
|
|
280
|
+
once per output pixel, not once per real photosite, so in deep shadows the noise is lower than a real sensor's
|
|
281
|
+
binned sum (√n times the read noise for n photosites). Tested by photon transfer at 150, 458 and 917 µm, ISO
|
|
282
|
+
12 800: the code variance matches N_e/K² + σ_read²/K² + 1/12 within 12 % (EMVA 1288 Release 4.0, §2, free).
|
|
283
|
+
- **Sampling.** The default point aperture samples one point of each square, so detail finer than the pitch
|
|
284
|
+
aliases at full contrast. Use `sensor.pixelAperture: "box"` (or `{ kind: "box", samplesPerAxis }` up to 16) to
|
|
285
|
+
average over the square, or the four-spot filter, whose separation scales with the pitch.
|
|
286
|
+
- **Diffraction.** The Airy pattern (a few µm) is far smaller than a pixel. The default kernel is 3 × 3 sites and
|
|
287
|
+
the centre site keeps 1 − 8√2/(π³β) of the energy, β = pitch/(λ·N_w): 99.96 % at 780 nm, f/1.4 and 917 µm.
|
|
288
|
+
Cells larger than 192 cut-off periods are integrated in real space (see [PSF foundation](https://github.com/Photivra/photivra/blob/main/docs/PSF_FOUNDATION.md#diffraction-limited-sampled-profile-21)),
|
|
289
|
+
so a 48 × 32 view of a 44 mm frame with `lens.diffraction` at f/1.4 took 1.0 s (the frequency-domain cells took
|
|
290
|
+
296 s there; both measured on the same machine in Node).
|
|
291
|
+
- **Motion, rolling shutter, anti-aliasing filter, AHD and the box aperture** follow the same angles at any pitch:
|
|
292
|
+
a yaw of ω moves the image ω·f/pitch pixels per second. A test renders each of them at 917 µm and at 9.17 µm with
|
|
293
|
+
the same 50 mm-equivalent prime (so f scales with the frame) and finds the same image within the small pitch's
|
|
294
|
+
own noise (mean difference under 1 code; AHD under 2.5).
|
|
295
|
+
- **Region tiles** stay byte-identical to the whole render (tested with diffraction, the four-spot filter, the box
|
|
296
|
+
aperture, AHD, rotation and a rolling shutter at 640 and 917 µm).
|
|
297
|
+
- **WebGPU.** `simulateCapture` runs on the CPU. The optional `webgpu-f32` sensor stage of
|
|
298
|
+
`createImageKernelAccelerator` handles capacities up to 2^24 e⁻, which the tier's 60 000 e⁻ reach at 100 µm; above
|
|
299
|
+
that it fails closed with `unsupported-float32-domain` (see [WebGPU backend](https://github.com/Photivra/photivra/blob/main/docs/WEBGPU.md)). The `cpu-reference`
|
|
300
|
+
backend runs it. The convolution, temporal-sum and chart stages work in sites and frame units, so the pitch does
|
|
301
|
+
not enter them.
|
|
302
|
+
|
|
303
|
+
Requests at 0.5–100 µm are byte-identical to 2.4.1.
|
|
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
|
+
|
|
211
309
|
### ISO and pixel size
|
|
212
310
|
|
|
213
311
|
The tier's ISO signal chain (conversion gain, saturation signal and read noise per ISO setting) is
|
|
@@ -226,7 +324,8 @@ independent of pixel size, so the same exposure gives the same image brightness
|
|
|
226
324
|
What you see: brightness stays the same, and per-pixel noise rises as pixels shrink, because a
|
|
227
325
|
smaller pixel collects fewer electrons for the same exposure (shot-noise SNR = √N_e). At f/8, 1/125 s,
|
|
228
326
|
ISO 100 on an 18 % grey card, the 8-bit sRGB mean is 101.2 at 30, 6 and 3 µm, and the green standard
|
|
229
|
-
deviation is 0.18, 0.93 and 1.83 codes.
|
|
327
|
+
deviation is 0.18, 0.93 and 1.83 codes. At whole-frame pitches (above 100 µm) the per-pixel noise falls below one
|
|
328
|
+
code; see [Whole-frame views](#whole-frame-views-25). Below about 1 µm the 8-bit sRGB mean falls slightly (about 1 code at 0.55 µm)
|
|
230
329
|
although the linear RAW mean is unchanged (within 0.3 %): strong noise passed through the concave
|
|
231
330
|
sRGB curve lowers the mean of the encoded values.
|
|
232
331
|
|
|
@@ -601,6 +700,14 @@ The optics options feed only what the environment site contract accepts:
|
|
|
601
700
|
lit circular pupil (scalar Fraunhofer diffraction), aberration-free and in focus, with pupil diameter
|
|
602
701
|
f/N and the thin-lens image distance (working f-number N·v/f). Each kernel holds the PSF energy of
|
|
603
702
|
site-sized cells, one kernel per spectral node.
|
|
703
|
+
- Sources: the Airy pattern is the textbook Fraunhofer diffraction pattern of a circular aperture
|
|
704
|
+
(Born & Wolf, *Principles of Optics*, 7th ed., §8.5.2; Goodman, *Introduction to Fourier Optics*,
|
|
705
|
+
4th ed., §4.4). The kernels come from the pupil's incoherent optical transfer function (Goodman,
|
|
706
|
+
4th ed., §7.3; MIT OpenCourseWare 2.71, lectures 21–22, free). The first dark ring is at
|
|
707
|
+
1.22·λ·N_w (j₁,₁/π with j₁,₁ = 3.8317, the first zero of J₁), so the Airy disk diameter is
|
|
708
|
+
2.44·λ·N_w, and the energy inside radius r is 1 − J₀(x)² − J₁(x)², x = π·r/(λ·N_w) (Wyant &
|
|
709
|
+
Creath 1992, §II, eqs. 1–4, free). The Bessel-function relations and expansions come from the
|
|
710
|
+
NIST DLMF (free). Full references are under [Sources](#sources).
|
|
604
711
|
- The kernel window reaches the third dark ring at the longest node by default (2, 4 and 7 sites at
|
|
605
712
|
f/4, f/8 and f/16 with 6 µm pixels); set `kernelRadiusSamples` (0–32) to change it. The Airy tail
|
|
606
713
|
outside the window (2–6 % of the energy with the default) is listed per wavelength in the
|
|
@@ -842,6 +949,8 @@ Ratios below about 1.1× are within the run-to-run spread (the three default run
|
|
|
842
949
|
(anti-aliasing spots, pupil rays, PSF taps, temporal samples) multiply the run time by about the same
|
|
843
950
|
factor, and output-side options cost almost nothing.
|
|
844
951
|
|
|
952
|
+
The cost of `simulateDisplayView` is in [Display view](#display-view).
|
|
953
|
+
|
|
845
954
|
### Measuring cost on the user's device
|
|
846
955
|
|
|
847
956
|
From 2.1 every result says what ran and how long each stage took, so an application can decide on the
|
|
@@ -960,10 +1069,16 @@ How it stays identical:
|
|
|
960
1069
|
own edges the region raster ends exactly where the whole frame's does, so `borderHandling: "mirror"`
|
|
961
1070
|
mirrors at the same place. AHD reads at most 5 sites away, so its edge-directed choices see the same
|
|
962
1071
|
neighbourhood.
|
|
963
|
-
- **Diffraction:** the object-space convolution
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
1072
|
+
- **Diffraction (2.6: region-limited):** the object-space convolution covers only the scene lattice the
|
|
1073
|
+
region's native sites query: the native rectangle plus the anti-aliasing and box-aperture offsets on
|
|
1074
|
+
each side, in bands of at most 32 native rows, with the kernel radius of real scene radiance around it.
|
|
1075
|
+
Each kept value is the same gather as in the whole render (the same kernel, the same taps in the same
|
|
1076
|
+
order, and every tap inside the grid), so the values are bit-identical while the work follows the
|
|
1077
|
+
region plus its halo instead of the frame width. In 2.3–2.5 the bands spanned the whole frame width, so
|
|
1078
|
+
a small crop of a wide frame paid for the full width and k tile columns repeated it k times. The noise
|
|
1079
|
+
needs no frame-wide state: each site's stream is seeded by a hash of the capture seed and its absolute
|
|
1080
|
+
native index (`seedSchedule: "murmur3-capture-seed-native-index-stream-v2"`), so it is counter-based,
|
|
1081
|
+
not a position in one frame-wide stream; the demosaic and the filters read only the margin above.
|
|
967
1082
|
|
|
968
1083
|
Measured on one core (Node 24, 4-vCPU Intel Xeon 2.80 GHz, built `dist`, one run each): a 256 × 256
|
|
969
1084
|
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
|
|
@@ -971,6 +1086,25 @@ one after another; with `lens.diffraction` 10.2 s whole and 12.9 s as four tiles
|
|
|
971
1086
|
convolution bands). A 64 × 64 crop of a 4000 × 3000 frame took 0.63 s, the same as a plain 64 × 64
|
|
972
1087
|
capture. Tiles in parallel workers divide the wall time by the number of cores.
|
|
973
1088
|
|
|
1089
|
+
**A 100 % crop with the slow options (2.6).** A 192 × 128 region at the centre of a 6000 × 4000 frame at 6 µm,
|
|
1090
|
+
Siemens star, f/16, `output.maximumPixels: NATIVE_RAW_LIMITS.ceilingPixels` (one fresh Node 24.21.0 process per
|
|
1091
|
+
row, 4-vCPU Intel Xeon 2.80 GHz, built `dist`, one run each; `diagnostics.timingsMs`):
|
|
1092
|
+
|
|
1093
|
+
| Options | 2.5.0 total (convolution) | 2.6 total (convolution) | Peak resident memory, 2.5.0 → 2.6 |
|
|
1094
|
+
| --- | --- | --- | --- |
|
|
1095
|
+
| defaults | 3.6 s (—) | 3.6 s (—) | 320 → 304 MB |
|
|
1096
|
+
| diffraction, four-spot filter, AHD | 60.8 s (50.2 s) | 11.8 s (1.7 s) | 556 → 379 MB |
|
|
1097
|
+
| diffraction, four-spot filter, AHD, box 2 × 2 | 228.7 s (195.8 s) | 39.6 s (6.5 s) | 1 285 → 545 MB |
|
|
1098
|
+
|
|
1099
|
+
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,
|
|
1100
|
+
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
|
|
1101
|
+
scene queries per site (20 spectral nodes × the filter spots × the box nodes: 80 per site with the filter, 320
|
|
1102
|
+
with the filter and the 2 × 2 box) and with the region's sites, not with the frame.
|
|
1103
|
+
|
|
1104
|
+
`output.maximumPixels` is still needed for a region of a frame wider or taller than 4094 pixels: the request,
|
|
1105
|
+
plan and noise seeds are those of the whole frame, and the whole frame's native raster is checked against the
|
|
1106
|
+
limit. The allocations follow the region, so the opt-in costs no memory for the rest of the frame.
|
|
1107
|
+
|
|
974
1108
|
`result.image` holds the region, `result.raw` and `result.plan` describe the region's native sites (the
|
|
975
1109
|
plan's active area is the region; `result.region.nativeRect` gives them in the whole frame), and
|
|
976
1110
|
`result.region` reports the rectangle and the frame. `diagnostics.options` lists `output.region`.
|
|
@@ -1032,6 +1166,81 @@ const [capture, reference] = await Promise.all([simulateCapture(request), simula
|
|
|
1032
1166
|
console.log(reference.kind, reference.label, capture.image.samples.length === reference.image.samples.length);
|
|
1033
1167
|
```
|
|
1034
1168
|
|
|
1169
|
+
## Display view
|
|
1170
|
+
|
|
1171
|
+
`simulateDisplayView(options, view)` returns what the real full-resolution capture of `options` would look like
|
|
1172
|
+
resized to `view.width` × `view.height`, at a cost that follows the display pixels. Pass the same request you use
|
|
1173
|
+
for the export, with the real sensor's pixel count and pitch:
|
|
1174
|
+
|
|
1175
|
+
```ts
|
|
1176
|
+
import { NATIVE_RAW_LIMITS, simulateCapture, simulateDisplayView, type SimulateCaptureOptions } from "@photivra/engine";
|
|
1177
|
+
|
|
1178
|
+
const request: SimulateCaptureOptions = { scene: { preset: "dead-leaves" }, lens: { prime: 35, fNumber: 2.8 }, subjectDistanceM: 3,
|
|
1179
|
+
sensor: { pixelPitchMicrometers: 6 }, output: { width: 6000, height: 4000, maximumPixels: NATIVE_RAW_LIMITS.ceilingPixels } };
|
|
1180
|
+
const preview = await simulateDisplayView(request, { width: 384, height: 256 }); // on screen
|
|
1181
|
+
console.log(preview.kind, preview.label, preview.diagnostics.timingsMs.view);
|
|
1182
|
+
// The export stays a real capture: simulateCapture(request), or tiles of it with output.region.
|
|
1183
|
+
const crop = await simulateCapture({ ...request, output: { ...request.output, region: { x: 2968, y: 1968, width: 64, height: 64 } } });
|
|
1184
|
+
console.log(crop.image.width);
|
|
1185
|
+
```
|
|
1186
|
+
|
|
1187
|
+
**What each display pixel is.** The area-weighted mean, in linear light, of the real output pixels its rectangle covers
|
|
1188
|
+
(6000 ÷ 384 = 15.625 real pixels per axis here), sRGB-encoded and rounded: area (box) resampling, the standard
|
|
1189
|
+
reduction filter. Lens diffraction, the anti-aliasing filter and the pixel aperture act at the real 6 µm pitch; the
|
|
1190
|
+
colour mosaic, demosaic and development are those of the real pixels; the noise is that of the average of the real
|
|
1191
|
+
pixels, demosaic correlation included; exposure, white balance, tone curve and framing are the capture's (the same code
|
|
1192
|
+
resolves them).
|
|
1193
|
+
|
|
1194
|
+
**How it is computed** (details and error analysis in [DISPLAY_VIEW.md](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md)):
|
|
1195
|
+
|
|
1196
|
+
- The expected value of each display pixel is a stratified Monte Carlo average over K × K real pixels of its rectangle
|
|
1197
|
+
(`view.samplesPerAxis`, default 16), seeded and unbiased. For each sampled pixel the expected electrons of the real
|
|
1198
|
+
sites its demosaic reads are exact, and the clips and tone curve act on the pixel's own noise distribution. A pilot
|
|
1199
|
+
grid gives uniform areas K/4 samples per axis; their error is zero anyway.
|
|
1200
|
+
- The noise is one correlated normal draw per display pixel with the covariance of the area mean of the real pixels'
|
|
1201
|
+
independent site noises (shot, dark, read, ADC), including the demosaic's correlation.
|
|
1202
|
+
- AHD is represented by its two linear candidates; where the noise is large enough for its homogeneity choice to shift
|
|
1203
|
+
the colour, a control-variate correction runs the engine's AHD steps on drawn noisy neighbourhoods.
|
|
1204
|
+
|
|
1205
|
+
Validated against real full-resolution captures resized in linear light: three sensors (12 MP phone, 24 MP full frame,
|
|
1206
|
+
100 MP medium format), five charts, f/2.8–f/16, ISO 100–6400, 96–800 display pixels wide, Speed and Quality options (see
|
|
1207
|
+
[Validation results](https://github.com/Photivra/photivra/blob/main/docs/DISPLAY_VIEW.md#validation-results)).
|
|
1208
|
+
|
|
1209
|
+
**Cost.** It follows the detailed display pixels × `samplesPerAxis`², not the real pixel count. Measured against the
|
|
1210
|
+
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;
|
|
1211
|
+
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)):
|
|
1212
|
+
|
|
1213
|
+
| Sensor | Options | 96 wide | 384 wide | 800 wide |
|
|
1214
|
+
| --- | --- | --- | --- | --- |
|
|
1215
|
+
| 24 MP full frame, 6 µm | Speed (defaults) | 2.4 s (2.3×) | 10.6 s (0.92×) | 26.6 s (0.65×) |
|
|
1216
|
+
| 12 MP phone, 1.4 µm | Speed | 2.2 s (2.4×) | 11.7 s (0.92×) | 32.1 s (0.59×) |
|
|
1217
|
+
| 100 MP medium format, 3.76 µm | Speed | 2.7 s (2.3×) | 15.8 s (1.2×) | 32.6 s (0.66×) |
|
|
1218
|
+
| 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×) |
|
|
1219
|
+
| 12 MP phone | Quality | 49.7 s (20×) | 106 s (3.8×) | 272 s (2.2×) |
|
|
1220
|
+
| 100 MP medium format | Quality | 18.2 s (3.7×) | 49.7 s (1.8×) | 126 s (1.0×) |
|
|
1221
|
+
|
|
1222
|
+
Quality costs more on small pixels. The diffraction kernel spans several real sites, and at 1.4 µm even ISO 100 is
|
|
1223
|
+
noisy enough for the AHD correction to run on every sampled pixel; its correction states are kept for later views of
|
|
1224
|
+
the same sensor, exposure and development in the same JavaScript realm (a second 384-wide phone view of the same
|
|
1225
|
+
request took 73 s instead of 103 s). The results are byte-identical to those of the display view as first merged,
|
|
1226
|
+
which took 222 s, 450 s and 837 s on the phone.
|
|
1227
|
+
|
|
1228
|
+
**Options.** `view.width` and `view.height` (1 to the capture's output size), `view.samplesPerAxis` (1–32),
|
|
1229
|
+
`view.format` (`"srgb-8"` default or `"srgb-16"`) and `view.region` (a rectangle in display pixels, byte-identical to
|
|
1230
|
+
the same pixels of the whole view, for tiling across workers). The capture's file-format options (`format`,
|
|
1231
|
+
`colorEncoding`, `bitDepth`, `gamutMapping`, `jpegQuantizationStep`, `dng`, `metadata`) do not apply. The request must
|
|
1232
|
+
be one `simulateCapture` accepts (a frame wider or taller than 4094 pixels needs `output.maximumPixels`, although the
|
|
1233
|
+
view allocates nothing at full resolution).
|
|
1234
|
+
|
|
1235
|
+
**Not supported** (refused with an `InvalidConfigurationError`): `lens.pupilSampling`, `lens.projection`,
|
|
1236
|
+
`lens.distortion`, `lens.vignetting`, `lens.psf`, camera rotation, `output.whiteBalance: "auto"`, the luminance tone
|
|
1237
|
+
curves, `output.highlightMode: "reconstruct"` and `output.region` (use `view.region`). `onProgress` is not called;
|
|
1238
|
+
`signal` cancels.
|
|
1239
|
+
|
|
1240
|
+
The result is labelled `kind: "display-view"` with a `label` for the user interface; it has no RAW, plan or file, and
|
|
1241
|
+
the `view.*` assumptions state the definition, the estimator, the noise model, the demosaic treatment and which capture
|
|
1242
|
+
options were ignored. The whole-frame views of [Whole-frame views](#whole-frame-views-25) are unchanged.
|
|
1243
|
+
|
|
1035
1244
|
## Not available
|
|
1036
1245
|
|
|
1037
1246
|
The facade offers no atmospheric haze or polarizer (see [Optics](#optics)), flash, video, HDR brackets
|
|
@@ -1040,6 +1249,33 @@ provided, for rights reasons.
|
|
|
1040
1249
|
|
|
1041
1250
|
## Sources
|
|
1042
1251
|
|
|
1252
|
+
- Diffraction (`lens.diffraction`, `createDiffractionLimitedPsfProfile`): M. Born, E. Wolf,
|
|
1253
|
+
*Principles of Optics*, 7th (expanded) ed., Cambridge University Press, 1999,
|
|
1254
|
+
doi:10.1017/CBO9781139644181, §8.5.2 "The circular aperture" (Fraunhofer diffraction at a circular
|
|
1255
|
+
aperture: the Airy pattern and its dark rings); J. W. Goodman, *Introduction to Fourier Optics*,
|
|
1256
|
+
4th ed., W. H. Freeman, 2017, §4.4 (examples of Fraunhofer diffraction patterns, including the
|
|
1257
|
+
circular aperture) and §7.3 (frequency response for diffraction-limited incoherent imaging: the
|
|
1258
|
+
optical transfer function as the normalized pupil autocorrelation); J. C. Wyant, K. Creath, "Basic
|
|
1259
|
+
Wavefront Aberration Theory for Optical Metrology", *Applied Optics and Optical Engineering* XI,
|
|
1260
|
+
ch. 1, Academic Press, 1992, §II "Aberration-Free Image", eqs. 1–4, free author copy at
|
|
1261
|
+
<https://wp.optics.arizona.edu/jcwyant/wp-content/uploads/sites/13/2016/08/03-BasicAberrations_and_Optical_Testing.pdf>
|
|
1262
|
+
(the Airy irradiance at the effective f-number, the 2.44·λ·N Airy disk diameter and the encircled
|
|
1263
|
+
energy 1 − J₀² − J₁²); MIT OpenCourseWare 2.71 Optics (Spring 2009), lectures 21–22, free at
|
|
1264
|
+
<https://ocw.mit.edu/courses/2-71-optics-spring-2009/resources/mit2_71s09_lec22/> (the intensity PSF as the transform of the
|
|
1265
|
+
pupil autocorrelation and the circular-pupil MTF); F. W. J. Olver et al. (eds.), *NIST Digital
|
|
1266
|
+
Library of Mathematical Functions*, release 1.2.8 (2026-09-15), free at <https://dlmf.nist.gov/>,
|
|
1267
|
+
§10.6(i) (eqs. 10.6.2 and 10.6.3: J₀′ = −J₁ and J₁′ = J₀ − J₁/x, from which the encircled energy
|
|
1268
|
+
follows), eq. 10.17.3 and §10.17(iii) (Hankel's large-argument expansion of J₀ and J₁ and its error
|
|
1269
|
+
bound, used for cells much larger than the pattern) and eq. 10.9.2 (Bessel's integral, used as an
|
|
1270
|
+
independent test). The original analysis is G. B. Airy, "On the Diffraction of an Object-glass with
|
|
1271
|
+
Circular Aperture", *Transactions of the Cambridge Philosophical Society* 5, 1835, pp. 283–291. The
|
|
1272
|
+
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)).
|
|
1273
|
+
- Print planning (`calculatePrintPlan`, `calculatePrintSizeLimit`): Z. Alomar, K. D. Bojikian, "Visual Acuity
|
|
1274
|
+
Testing in Adults", EyeWiki, American Academy of Ophthalmology, last edited June 16, 2026, free to read at
|
|
1275
|
+
<https://eyewiki.aao.org/Visual_Acuity_Testing_in_Adults> (the minimum angle of resolution of an ideal eye under optimal
|
|
1276
|
+
conditions is about 1 minute of arc; 20/20 letters are built from 1-arcmin elements). One arcminute per pixel is normal
|
|
1277
|
+
(20/20) acuity under optimal conditions and an approximate rule of thumb: some viewers resolve finer detail and many
|
|
1278
|
+
coarser. The planner uses the criterion the caller declares.
|
|
1043
1279
|
- R. Palum, "Anti-Aliasing Filter Analysis for Digital Cameras", Proc. IS&T International Congress of
|
|
1044
1280
|
Imaging Science (ICIS '06), 2006, free at
|
|
1045
1281
|
<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.6.0" as const
|
|
15
15
|
```
|