@photivra/engine 2.20.0 → 2.21.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 +178 -1
- package/CITATION.cff +1 -1
- package/README.md +7 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +7 -0
- package/dist/capture/dng-lens-correction-opcodes.d.ts +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js +2 -2
- package/dist/capture/dng-lens-correction-opcodes.js.map +1 -1
- package/dist/capture/exposure-build-up-stage.d.ts +16 -0
- package/dist/capture/exposure-build-up-stage.js +4 -0
- package/dist/capture/exposure-build-up-stage.js.map +1 -0
- package/dist/capture/native-environment-lean.d.ts +2 -1
- package/dist/capture/native-environment-lean.js +41 -73
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-tabulated.js +4 -3
- package/dist/capture/native-environment-tabulated.js.map +1 -1
- package/dist/capture/native-raw.d.ts +3 -2
- package/dist/capture/native-raw.js +5 -4
- package/dist/capture/native-raw.js.map +1 -1
- package/dist/capture/sensor-raw-producer.d.ts +4 -1
- package/dist/capture/sensor-raw-producer.js +12 -3
- package/dist/capture/sensor-raw-producer.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/equipment/generic-lens-catalogue.d.ts +3 -2
- package/dist/equipment/generic-lens-catalogue.js +18 -5
- package/dist/equipment/generic-lens-catalogue.js.map +1 -1
- package/dist/equipment/generic-tier-assets.d.ts +32 -0
- package/dist/equipment/generic-tier-assets.js +16 -11
- package/dist/equipment/generic-tier-assets.js.map +1 -1
- package/dist/equipment/sensor-profile-body.d.ts +78 -0
- package/dist/equipment/sensor-profile-body.js +77 -0
- package/dist/equipment/sensor-profile-body.js.map +1 -0
- package/dist/equipment/sensor-profiles.d.ts +8 -0
- package/dist/equipment/sensor-profiles.js +82 -0
- package/dist/equipment/sensor-profiles.js.map +1 -0
- package/dist/facade/catalogue-lens-signal.d.ts +2 -2
- package/dist/facade/catalogue-lens-signal.js +58 -71
- package/dist/facade/catalogue-lens-signal.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +2 -1
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/defocus-image-space-provider.d.ts +7 -0
- package/dist/facade/defocus-image-space-provider.js +64 -76
- package/dist/facade/defocus-image-space-provider.js.map +1 -1
- package/dist/facade/display-view-gpu-session.js +2 -0
- package/dist/facade/display-view-gpu-session.js.map +1 -1
- package/dist/facade/display-view-gpu.d.ts +6 -1
- package/dist/facade/display-view-gpu.js.map +1 -1
- package/dist/facade/display-view-lens-signal.js +25 -23
- package/dist/facade/display-view-lens-signal.js.map +1 -1
- package/dist/facade/display-view.js +5 -23
- package/dist/facade/display-view.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +3 -1
- package/dist/facade/expected-signal-setup.js +1 -1
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/expected-signal.d.ts +47 -14
- package/dist/facade/expected-signal.js +117 -112
- package/dist/facade/expected-signal.js.map +1 -1
- package/dist/facade/exposure-build-up.d.ts +70 -0
- package/dist/facade/exposure-build-up.js +164 -0
- package/dist/facade/exposure-build-up.js.map +1 -0
- package/dist/facade/fast-methods.js +9 -1
- package/dist/facade/fast-methods.js.map +1 -1
- package/dist/facade/generic-lens-capture.d.ts +2 -2
- package/dist/facade/generic-lens-capture.js +2 -2
- package/dist/facade/generic-lens-capture.js.map +1 -1
- package/dist/facade/image-plane-enable.d.ts +7 -0
- package/dist/facade/image-plane-enable.js +17 -0
- package/dist/facade/image-plane-enable.js.map +1 -0
- package/dist/facade/image-plane-lattice-data.d.ts +6 -0
- package/dist/facade/image-plane-lattice-data.js +10 -0
- package/dist/facade/image-plane-lattice-data.js.map +1 -0
- package/dist/facade/image-plane-support.d.ts +33 -0
- package/dist/facade/image-plane-support.js +11 -0
- package/dist/facade/image-plane-support.js.map +1 -0
- package/dist/facade/image-plane.d.ts +36 -0
- package/dist/facade/image-plane.js +353 -0
- package/dist/facade/image-plane.js.map +1 -0
- package/dist/facade/mixed-illumination.d.ts +108 -0
- package/dist/facade/mixed-illumination.js +174 -0
- package/dist/facade/mixed-illumination.js.map +1 -0
- package/dist/facade/representative-light-sources.d.ts +72 -0
- package/dist/facade/representative-light-sources.js +124 -0
- package/dist/facade/representative-light-sources.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +129 -4
- package/dist/facade/scene-radiance.js +48 -13
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +39 -10
- package/dist/facade/simulate-capture.js +252 -92
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.js +14 -6
- package/dist/facade/simulate-display-view.js.map +1 -1
- package/dist/gpu/chart-composite-gpu.d.ts +13 -1
- package/dist/gpu/chart-composite-gpu.js +48 -9
- package/dist/gpu/chart-composite-gpu.js.map +1 -1
- package/dist/gpu/chart-radiance-gpu.js +4 -30
- package/dist/gpu/chart-radiance-gpu.js.map +1 -1
- package/dist/gpu/display-view-composite-material-module.d.ts +10 -4
- package/dist/gpu/display-view-composite-material-module.js +39 -13
- package/dist/gpu/display-view-composite-material-module.js.map +1 -1
- package/dist/gpu/display-view-image-material-module.d.ts +86 -0
- package/dist/gpu/display-view-image-material-module.js +339 -0
- package/dist/gpu/display-view-image-material-module.js.map +1 -0
- package/dist/gpu/display-view-image-quality.d.ts +45 -0
- package/dist/gpu/display-view-image-quality.js +380 -0
- package/dist/gpu/display-view-image-quality.js.map +1 -0
- package/dist/gpu/display-view-quality-gpu.d.ts +67 -1
- package/dist/gpu/display-view-quality-gpu.js +136 -205
- package/dist/gpu/display-view-quality-gpu.js.map +1 -1
- package/dist/gpu/display-view-speed-gpu.d.ts +51 -3
- package/dist/gpu/display-view-speed-gpu.js +131 -93
- package/dist/gpu/display-view-speed-gpu.js.map +1 -1
- package/dist/gpu/wgsl-display-view-quality-kernel.d.ts +29 -15
- package/dist/gpu/wgsl-display-view-quality-kernel.js +58 -36
- package/dist/gpu/wgsl-display-view-quality-kernel.js.map +1 -1
- package/dist/gpu/wgsl-display-view-speed-kernel.d.ts +21 -6
- package/dist/gpu/wgsl-display-view-speed-kernel.js +46 -11
- package/dist/gpu/wgsl-display-view-speed-kernel.js.map +1 -1
- package/dist/gpu/wgsl-exact-sum.d.ts +20 -0
- package/dist/gpu/wgsl-exact-sum.js +42 -0
- package/dist/gpu/wgsl-exact-sum.js.map +1 -0
- package/dist/index.d.ts +11 -3
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/motion/subject-motion.d.ts +66 -0
- package/dist/motion/subject-motion.js +52 -0
- package/dist/motion/subject-motion.js.map +1 -0
- package/dist/optics/generic-lens-lateral-color.d.ts +83 -0
- package/dist/optics/generic-lens-lateral-color.js +77 -0
- package/dist/optics/generic-lens-lateral-color.js.map +1 -0
- package/dist/optics/generic-lens-psf.js +54 -46
- package/dist/optics/generic-lens-psf.js.map +1 -1
- package/dist/optics/lens-psf-profile.d.ts +7 -0
- package/dist/optics/lens-psf-profile.js +2 -1
- package/dist/optics/lens-psf-profile.js.map +1 -1
- package/dist/output/developed-lens-corrections.d.ts +19 -5
- package/dist/output/developed-lens-corrections.js +28 -9
- package/dist/output/developed-lens-corrections.js.map +1 -1
- package/dist/output/generic-lens-corrections.d.ts +41 -5
- package/dist/output/generic-lens-corrections.js +52 -8
- package/dist/output/generic-lens-corrections.js.map +1 -1
- package/dist/output/print-plan.d.ts +41 -5
- package/dist/output/print-plan.js +59 -14
- package/dist/output/print-plan.js.map +1 -1
- package/dist/sensor/binomial-sample.d.ts +3 -0
- package/dist/sensor/binomial-sample.js +86 -0
- package/dist/sensor/binomial-sample.js.map +1 -0
- package/dist/sensor/sensor-characteristics.d.ts +10 -0
- package/dist/sensor/sensor-characteristics.js +20 -9
- package/dist/sensor/sensor-characteristics.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +98 -1
- package/docs/QUICK_START.md +125 -0
- package/docs/USAGE.md +92 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/equipment-generic-lens-catalogue.md +10 -5
- package/docs/api/equipment-sensor-profile-body.md +64 -0
- package/docs/api/equipment-sensor-profiles.md +86 -0
- package/docs/api/exports.json +207 -2
- package/docs/api/facade-exposure-build-up.md +105 -0
- package/docs/api/facade-fast-methods.md +9 -1
- package/docs/api/facade-image-plane-enable.md +16 -0
- package/docs/api/facade-mixed-illumination.md +67 -0
- package/docs/api/facade-representative-light-sources.md +131 -0
- package/docs/api/facade-scene-radiance.md +54 -3
- package/docs/api/facade-simulate-capture.md +26 -6
- package/docs/api/motion-subject-motion.md +23 -0
- package/docs/api/optics-generic-lens-lateral-color.md +79 -0
- package/docs/api/output-developed-lens-corrections.md +10 -1
- package/docs/api/output-generic-lens-corrections.md +40 -4
- package/docs/api/output-print-plan.md +35 -1
- package/llms.txt +2 -0
- package/package.json +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { type GenericTierBodyAssets, type GenericTierPresetVersion } from "./generic-tier-assets.js";
|
|
2
|
+
/** How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value. */
|
|
3
|
+
export type SensorProfileValueKind = "published" | "derived" | "design";
|
|
4
|
+
/** One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind. */
|
|
5
|
+
export interface SensorProfile {
|
|
6
|
+
id: string;
|
|
7
|
+
/** Format, resolution, architecture and era; no maker or product name. */
|
|
8
|
+
name: string;
|
|
9
|
+
/** A market category, not a specification: it is a label and changes nothing. */
|
|
10
|
+
marketTag: "consumer" | "professional";
|
|
11
|
+
architecture: "front-illuminated" | "back-illuminated" | "stacked";
|
|
12
|
+
sensorWidthMm: number;
|
|
13
|
+
sensorHeightMm: number;
|
|
14
|
+
pixelsWide: number;
|
|
15
|
+
pixelsHigh: number;
|
|
16
|
+
pixelPitchMicrometers: number;
|
|
17
|
+
/** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */
|
|
18
|
+
peakQuantumEfficiency: number;
|
|
19
|
+
/** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */
|
|
20
|
+
fullWellElectrons: number;
|
|
21
|
+
/** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */
|
|
22
|
+
readNoise: {
|
|
23
|
+
lowGainElectrons: number;
|
|
24
|
+
highGainElectrons: number;
|
|
25
|
+
highGainFromIso: number | null;
|
|
26
|
+
};
|
|
27
|
+
/** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */
|
|
28
|
+
highGainFullWellElectrons: number | null;
|
|
29
|
+
adcBitDepth: 12 | 14 | 16;
|
|
30
|
+
/** Re-rated base ISO (see the module comment) and the highest ISO setting. */
|
|
31
|
+
baseIso: number;
|
|
32
|
+
maximumIso: number;
|
|
33
|
+
/** Largest analog gain relative to base ISO; above it the gain is digital. */
|
|
34
|
+
maximumAnalogGain: number;
|
|
35
|
+
/** Rolling readout, s: the top-to-bottom skew with `motion.shutter: "rolling"`. */
|
|
36
|
+
readoutSeconds: number;
|
|
37
|
+
/** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */
|
|
38
|
+
fixedPattern: {
|
|
39
|
+
prnu: number;
|
|
40
|
+
dsnu: number;
|
|
41
|
+
hotPixelFraction: number;
|
|
42
|
+
hotPixelMultiplier: number;
|
|
43
|
+
rowElectrons: number;
|
|
44
|
+
columnElectrons: number;
|
|
45
|
+
};
|
|
46
|
+
/** What the profile is good for (text only). */
|
|
47
|
+
goodFor: readonly string[];
|
|
48
|
+
description: string;
|
|
49
|
+
/** "phone": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */
|
|
50
|
+
fixedLenses: "phone" | null;
|
|
51
|
+
/**
|
|
52
|
+
* Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md "Sources"); a value not listed is a design value.
|
|
53
|
+
* For example `fullWellElectrons: "published S1"` or `baseIso: "derived"`.
|
|
54
|
+
*/
|
|
55
|
+
provenance: Readonly<Partial<Record<keyof SensorProfile | "sensorSize" | "pixels" | "highGainFromIso", string>>>;
|
|
56
|
+
}
|
|
57
|
+
/** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */
|
|
58
|
+
export declare const SENSOR_PROFILE_ISO_MARKINGS: readonly number[];
|
|
59
|
+
/**
|
|
60
|
+
* The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than
|
|
61
|
+
* the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).
|
|
62
|
+
* Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.
|
|
63
|
+
*/
|
|
64
|
+
export declare function parseSensorProfile(value: unknown, path?: string): SensorProfile;
|
|
65
|
+
/** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */
|
|
66
|
+
export declare const SENSOR_PROFILE_ISO_ANCHOR: Readonly<{
|
|
67
|
+
iso: 100;
|
|
68
|
+
fullWellElectrons: 60000;
|
|
69
|
+
pixelAreaSquareMicrometers: 36;
|
|
70
|
+
peakQuantumEfficiency: 0.5;
|
|
71
|
+
}>;
|
|
72
|
+
/** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */
|
|
73
|
+
export declare function sensorProfileSaturationIso(p: Pick<SensorProfile, "peakQuantumEfficiency" | "pixelPitchMicrometers" | "fullWellElectrons">): number;
|
|
74
|
+
/**
|
|
75
|
+
* The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both
|
|
76
|
+
* read noises: √N for a site that sums N photosites (a downsized view, #554).
|
|
77
|
+
*/
|
|
78
|
+
export declare function sensorProfileBody(p: SensorProfile, body: GenericTierBodyAssets, version: GenericTierPresetVersion, readNoiseScale?: number): GenericTierBodyAssets;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Sensor profiles (#554): the profile record, its strict parser and the camera-body sensor assets built from it. The
|
|
4
|
+
* catalogue's data are in `sensor-profiles.ts`, which nothing in a capture imports: a caller passes a profile record
|
|
5
|
+
* (`simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`), so a capture bundle never contains the catalogue.
|
|
6
|
+
*
|
|
7
|
+
* ISO rating (owner decision, 2026-11-09): each profile's base ISO is re-rated from its full well, QE and pixel area,
|
|
8
|
+
* S0 = 100 · (η / 0.5) · (A / 36 µm²) · (60 000 e⁻ / FW), rounded to the nearest third-stop marking, so the RAW white at base
|
|
9
|
+
* ISO is the full well and every ISO keeps the engine's anchor (the Professional tier's ISO 100: 60 000 e⁻ at the white,
|
|
10
|
+
* 6 µm pixels, QE 0.5; S = 10/H, CIPA DC-004-2020 §3.2). The rounding is reported as `sensitivityOffsetStops` in the
|
|
11
|
+
* `sensor.profile` assumption. The high gain's charge storage is the white charge at its switch ISO.
|
|
12
|
+
* @see docs/SENSOR_PROFILES.md for every value's source or design basis.
|
|
13
|
+
*/
|
|
14
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
15
|
+
import { requireInputObject } from "../core/record-validation.js";
|
|
16
|
+
import { checkFieldRanges } from "../sensor/sensor-characteristics.js";
|
|
17
|
+
import { bodyAssets, GENERIC_THIRD_STOP_ISO_SETTINGS } from "./generic-tier-assets.js";
|
|
18
|
+
/** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */
|
|
19
|
+
export const SENSOR_PROFILE_ISO_MARKINGS = /* @__PURE__ */ Object.freeze([32, 40, 50, 64, 80, ...GENERIC_THIRD_STOP_ISO_SETTINGS, 32000, 40000, 51200, 64000, 80000, 102400]);
|
|
20
|
+
const ISO_MARKINGS = SENSOR_PROFILE_ISO_MARKINGS;
|
|
21
|
+
const fail = (message) => { throw new InvalidConfigurationError(message); };
|
|
22
|
+
const isoMarking = (v) => ISO_MARKINGS.includes(v);
|
|
23
|
+
const text = (v) => typeof v === "string" && v.length > 0 && v.length <= 400;
|
|
24
|
+
const FRACTION = [0, 1], ELECTRONS = [0, 100];
|
|
25
|
+
/** The record's fields, by path (#555's range checker: [minimum, maximum], the values allowed, 0 a nested record, or a predicate). */
|
|
26
|
+
const SCHEMA = { id: text, name: text, marketTag: ["consumer", "professional"], architecture: ["front-illuminated", "back-illuminated", "stacked"],
|
|
27
|
+
sensorWidthMm: [1, 100], sensorHeightMm: [1, 100], pixelsWide: [1, 1e5], pixelsHigh: [1, 1e5], pixelPitchMicrometers: [0.5, 100], peakQuantumEfficiency: [0.05, 0.9],
|
|
28
|
+
fullWellElectrons: [100, 1e10], readNoise: 0, "readNoise.lowGainElectrons": ELECTRONS, "readNoise.highGainElectrons": ELECTRONS, "readNoise.highGainFromIso": (v) => v === null || isoMarking(v),
|
|
29
|
+
highGainFullWellElectrons: (v) => v === null || typeof v === "number" && v >= 1 && v <= 1e10, adcBitDepth: [12, 14, 16], baseIso: isoMarking, maximumIso: isoMarking,
|
|
30
|
+
maximumAnalogGain: [1, 1024], readoutSeconds: [1e-6, 1], fixedPattern: 0, "fixedPattern.prnu": FRACTION, "fixedPattern.dsnu": [0, 100], "fixedPattern.hotPixelFraction": FRACTION,
|
|
31
|
+
"fixedPattern.hotPixelMultiplier": [1, 1e4], "fixedPattern.rowElectrons": ELECTRONS, "fixedPattern.columnElectrons": ELECTRONS,
|
|
32
|
+
goodFor: (v) => Array.isArray(v) && v.length <= 16 && v.every(text), description: text, fixedLenses: [null, "phone"],
|
|
33
|
+
provenance: (v) => typeof v === "object" && v !== null && !Array.isArray(v) && Object.entries(v).every(([k, s]) => k.length <= 64 && text(s)) };
|
|
34
|
+
/**
|
|
35
|
+
* The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than
|
|
36
|
+
* the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).
|
|
37
|
+
* Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.
|
|
38
|
+
*/
|
|
39
|
+
export function parseSensorProfile(value, path = "sensor.profile") {
|
|
40
|
+
requireInputObject(value, path);
|
|
41
|
+
checkFieldRanges(value, Object.keys(value), "", SCHEMA, path + ".", true);
|
|
42
|
+
const p = value, n = p.readNoise;
|
|
43
|
+
if (!Number.isInteger(p.pixelsWide) || !Number.isInteger(p.pixelsHigh))
|
|
44
|
+
fail(path + ".pixelsWide and pixelsHigh must be whole numbers.");
|
|
45
|
+
if (p.baseIso > p.maximumIso)
|
|
46
|
+
fail(path + ".baseIso must not exceed maximumIso.");
|
|
47
|
+
if (n.highGainFromIso === null ? p.highGainFullWellElectrons !== null || n.highGainElectrons !== n.lowGainElectrons :
|
|
48
|
+
p.highGainFullWellElectrons === null || n.highGainFromIso <= p.baseIso || p.highGainFullWellElectrons > p.fullWellElectrons)
|
|
49
|
+
fail(path + ": a dual-gain switch above baseIso needs highGainFullWellElectrons up to the full well; without one, equal read noises and a null storage.");
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
/** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */
|
|
53
|
+
export const SENSOR_PROFILE_ISO_ANCHOR = /* @__PURE__ */ Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 });
|
|
54
|
+
/** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */
|
|
55
|
+
export function sensorProfileSaturationIso(p) {
|
|
56
|
+
const a = SENSOR_PROFILE_ISO_ANCHOR;
|
|
57
|
+
return a.iso * p.peakQuantumEfficiency / a.peakQuantumEfficiency * p.pixelPitchMicrometers ** 2 / a.pixelAreaSquareMicrometers * a.fullWellElectrons / p.fullWellElectrons;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both
|
|
61
|
+
* read noises: √N for a site that sums N photosites (a downsized view, #554).
|
|
62
|
+
*/
|
|
63
|
+
export function sensorProfileBody(p, body, version, readNoiseScale = 1) {
|
|
64
|
+
const evidence = [{ sourceOrigin: "manufacturer", sourceReference: "photivra:sensor-profiles-v1#" + p.id, reuseStatus: "factual-reference-only",
|
|
65
|
+
note: "Published specifications and open papers (docs/SENSOR_PROFILES.md); unpublished values are Photivra design values." }];
|
|
66
|
+
const black = { 12: 256, 14: 1024, 16: 4096 }[p.adcBitDepth], n = p.readNoise;
|
|
67
|
+
const built = bodyAssets(version, "photivra-sensor-profile-" + p.id, p.maximumIso, 1, 1, { bitDepth: p.adcBitDepth, blackLevelCode: black,
|
|
68
|
+
saturationElectrons: p.fullWellElectrons, maximumAnalogGain: p.maximumAnalogGain, lowGainReadNoise: n.lowGainElectrons * readNoiseScale, highGainReadNoise: n.highGainElectrons * readNoiseScale,
|
|
69
|
+
highGainFromIso: n.highGainFromIso ?? Infinity, highGainMaximumCharge: (p.highGainFullWellElectrons ?? p.fullWellElectrons) * (1 + 1e-12),
|
|
70
|
+
prnu: p.fixedPattern.prnu, dsnu: p.fixedPattern.dsnu, hotFraction: p.fixedPattern.hotPixelFraction, hotMultiplier: p.fixedPattern.hotPixelMultiplier,
|
|
71
|
+
rowRms: p.fixedPattern.rowElectrons, columnRms: p.fixedPattern.columnElectrons, instanceSeed: (0x9e3779b9 ^ (p.pixelsWide * 8191 + p.pixelsHigh)) >>> 0 }, p.readoutSeconds, .5, .01, false, false, { isoValues: ISO_MARKINGS.filter((iso) => iso >= p.baseIso && iso <= p.maximumIso), evidence,
|
|
72
|
+
limitations: ["Sensor profile " + p.name + " (#554): published, derived and design values (docs/SENSOR_PROFILES.md); not a calibration of any camera."] });
|
|
73
|
+
// Only the sensor assets are taken; the placeholders above (release, stabilization …) are the body's own.
|
|
74
|
+
return { ...body, exposure: built.exposure, iso: built.iso, signalChain: built.signalChain, readout: built.readout, readoutTiming: built.readoutTiming,
|
|
75
|
+
rollingReadoutTiming: built.rollingReadoutTiming, fixedPatternNoise: built.fixedPatternNoise, flashSync: built.flashSync };
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=sensor-profile-body.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensor-profile-body.js","sourceRoot":"","sources":["../../src/equipment/sensor-profile-body.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor profiles (#554): the profile record, its strict parser and the camera-body sensor assets built from it. The\n * catalogue's data are in `sensor-profiles.ts`, which nothing in a capture imports: a caller passes a profile record\n * (`simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`), so a capture bundle never contains the catalogue.\n *\n * ISO rating (owner decision, 2026-11-09): each profile's base ISO is re-rated from its full well, QE and pixel area,\n * S0 = 100 · (η / 0.5) · (A / 36 µm²) · (60 000 e⁻ / FW), rounded to the nearest third-stop marking, so the RAW white at base\n * ISO is the full well and every ISO keeps the engine's anchor (the Professional tier's ISO 100: 60 000 e⁻ at the white,\n * 6 µm pixels, QE 0.5; S = 10/H, CIPA DC-004-2020 §3.2). The rounding is reported as `sensitivityOffsetStops` in the\n * `sensor.profile` assumption. The high gain's charge storage is the white charge at its switch ISO.\n * @see docs/SENSOR_PROFILES.md for every value's source or design basis.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { EvidenceProvenance } from \"../core/evidence-provenance.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { checkFieldRanges, type FieldRange } from \"../sensor/sensor-characteristics.js\";\nimport { bodyAssets, GENERIC_THIRD_STOP_ISO_SETTINGS, type GenericTierBodyAssets, type GenericTierPresetVersion } from \"./generic-tier-assets.js\";\n\n/** How a profile value was obtained: copied from a source, calculated from sourced values, or Photivra's design value. */\nexport type SensorProfileValueKind = \"published\" | \"derived\" | \"design\";\n\n/** One sensor profile. Every number is per photosite unless stated; see `provenance` for each value's kind. */\nexport interface SensorProfile {\n id: string;\n /** Format, resolution, architecture and era; no maker or product name. */\n name: string;\n /** A market category, not a specification: it is a label and changes nothing. */\n marketTag: \"consumer\" | \"professional\";\n architecture: \"front-illuminated\" | \"back-illuminated\" | \"stacked\";\n sensorWidthMm: number; sensorHeightMm: number;\n pixelsWide: number; pixelsHigh: number;\n pixelPitchMicrometers: number;\n /** The green channel's peak EQE (#555 `peakQuantumEfficiency`). */\n peakQuantumEfficiency: number;\n /** Full-well capacity, e⁻: the charge at the RAW white level at base ISO. */\n fullWellElectrons: number;\n /** Read noise, RMS e⁻; `highGainFromIso` null for one conversion gain throughout. */\n readNoise: { lowGainElectrons: number; highGainElectrons: number; highGainFromIso: number | null };\n /** Charge storage limit of the high conversion gain, e⁻ (null without one): full well × base ISO / switch ISO. */\n highGainFullWellElectrons: number | null;\n adcBitDepth: 12 | 14 | 16;\n /** Re-rated base ISO (see the module comment) and the highest ISO setting. */\n baseIso: number; maximumIso: number;\n /** Largest analog gain relative to base ISO; above it the gain is digital. */\n maximumAnalogGain: number;\n /** Rolling readout, s: the top-to-bottom skew with `motion.shutter: \"rolling\"`. */\n readoutSeconds: number;\n /** Fixed-pattern noise used with `sensor.fixedPatternNoise` (EMVA 1288 Release 4.0 non-uniformities). */\n fixedPattern: { prnu: number; dsnu: number; hotPixelFraction: number; hotPixelMultiplier: number; rowElectrons: number; columnElectrons: number };\n /** What the profile is good for (text only). */\n goodFor: readonly string[];\n description: string;\n /** \"phone\": the profile takes only the catalogue's phone lenses (`lens.catalogue`), which need it. */\n fixedLenses: \"phone\" | null;\n /**\n * Each value's kind and source key (S1–S10, docs/SENSOR_PROFILES.md \"Sources\"); a value not listed is a design value.\n * For example `fullWellElectrons: \"published S1\"` or `baseIso: \"derived\"`.\n */\n provenance: Readonly<Partial<Record<keyof SensorProfile | \"sensorSize\" | \"pixels\" | \"highGainFromIso\", string>>>;\n}\n\n/** Third-stop ISO markings from ISO 32 to 102 400 (S = 100·2^(k/3), rounded; the tiers' list in the middle): a profile's ISO settings. */\nexport const SENSOR_PROFILE_ISO_MARKINGS: readonly number[] = /* @__PURE__ */ Object.freeze([32, 40, 50, 64, 80, ...GENERIC_THIRD_STOP_ISO_SETTINGS, 32000, 40000, 51200, 64000, 80000, 102400]);\nconst ISO_MARKINGS = SENSOR_PROFILE_ISO_MARKINGS;\n\nconst fail = (message: string): never => { throw new InvalidConfigurationError(message); };\nconst isoMarking = (v: unknown): boolean => ISO_MARKINGS.includes(v as number);\nconst text = (v: unknown): boolean => typeof v === \"string\" && v.length > 0 && v.length <= 400;\nconst FRACTION = [0, 1] as const, ELECTRONS = [0, 100] as const;\n/** The record's fields, by path (#555's range checker: [minimum, maximum], the values allowed, 0 a nested record, or a predicate). */\nconst SCHEMA: Readonly<Record<string, FieldRange>> = { id: text, name: text, marketTag: [\"consumer\", \"professional\"], architecture: [\"front-illuminated\", \"back-illuminated\", \"stacked\"],\n sensorWidthMm: [1, 100], sensorHeightMm: [1, 100], pixelsWide: [1, 1e5], pixelsHigh: [1, 1e5], pixelPitchMicrometers: [0.5, 100], peakQuantumEfficiency: [0.05, 0.9],\n fullWellElectrons: [100, 1e10], readNoise: 0, \"readNoise.lowGainElectrons\": ELECTRONS, \"readNoise.highGainElectrons\": ELECTRONS, \"readNoise.highGainFromIso\": (v) => v === null || isoMarking(v),\n highGainFullWellElectrons: (v) => v === null || typeof v === \"number\" && v >= 1 && v <= 1e10, adcBitDepth: [12, 14, 16], baseIso: isoMarking, maximumIso: isoMarking,\n maximumAnalogGain: [1, 1024], readoutSeconds: [1e-6, 1], fixedPattern: 0, \"fixedPattern.prnu\": FRACTION, \"fixedPattern.dsnu\": [0, 100], \"fixedPattern.hotPixelFraction\": FRACTION,\n \"fixedPattern.hotPixelMultiplier\": [1, 1e4], \"fixedPattern.rowElectrons\": ELECTRONS, \"fixedPattern.columnElectrons\": ELECTRONS,\n goodFor: (v) => Array.isArray(v) && v.length <= 16 && v.every(text), description: text, fixedLenses: [null, \"phone\"],\n provenance: (v) => typeof v === \"object\" && v !== null && !Array.isArray(v) && Object.entries(v).every(([k, s]) => k.length <= 64 && text(s)) };\n/**\n * The profile record checked strictly: every field present, no unknown field, each value in range, base ISO no higher than\n * the maximum, and a dual-gain switch above base ISO with its storage limit (no switch: one read noise, no storage limit).\n * Fixed-pattern magnitudes and the ISO regimes are checked again by the engine's parsers when the body is built.\n */\nexport function parseSensorProfile(value: unknown, path = \"sensor.profile\"): SensorProfile {\n requireInputObject(value, path);\n checkFieldRanges(value as Record<string, unknown>, Object.keys(value as object), \"\", SCHEMA, path + \".\", true);\n const p = value as SensorProfile, n = p.readNoise;\n if (!Number.isInteger(p.pixelsWide) || !Number.isInteger(p.pixelsHigh)) fail(path + \".pixelsWide and pixelsHigh must be whole numbers.\");\n if (p.baseIso > p.maximumIso) fail(path + \".baseIso must not exceed maximumIso.\");\n if (n.highGainFromIso === null ? p.highGainFullWellElectrons !== null || n.highGainElectrons !== n.lowGainElectrons :\n p.highGainFullWellElectrons === null || n.highGainFromIso <= p.baseIso || p.highGainFullWellElectrons > p.fullWellElectrons)\n fail(path + \": a dual-gain switch above baseIso needs highGainFullWellElectrons up to the full well; without one, equal read noises and a null storage.\");\n return p;\n}\n\n/** The engine's ISO anchor (see the module comment): white charge 60 000 e⁻ at ISO 100, 36 µm², QE 0.5. */\nexport const SENSOR_PROFILE_ISO_ANCHOR = /* @__PURE__ */ Object.freeze({ iso: 100, fullWellElectrons: 60000, pixelAreaSquareMicrometers: 36, peakQuantumEfficiency: .5 });\n/** The unrounded re-rated base ISO S0 = 100 · (η/0.5) · (A/36 µm²) · (60 000 e⁻ / FW). */\nexport function sensorProfileSaturationIso(p: Pick<SensorProfile, \"peakQuantumEfficiency\" | \"pixelPitchMicrometers\" | \"fullWellElectrons\">): number {\n const a = SENSOR_PROFILE_ISO_ANCHOR;\n return a.iso * p.peakQuantumEfficiency / a.peakQuantumEfficiency * p.pixelPitchMicrometers ** 2 / a.pixelAreaSquareMicrometers * a.fullWellElectrons / p.fullWellElectrons;\n}\n\n\n/**\n * The profile's sensor assets on a camera body: the body's other capabilities are kept. `readNoiseScale` (≥ 1) multiplies both\n * read noises: √N for a site that sums N photosites (a downsized view, #554).\n */\nexport function sensorProfileBody(p: SensorProfile, body: GenericTierBodyAssets, version: GenericTierPresetVersion, readNoiseScale = 1): GenericTierBodyAssets {\n const evidence: readonly EvidenceProvenance[] = [{ sourceOrigin: \"manufacturer\", sourceReference: \"photivra:sensor-profiles-v1#\" + p.id, reuseStatus: \"factual-reference-only\",\n note: \"Published specifications and open papers (docs/SENSOR_PROFILES.md); unpublished values are Photivra design values.\" }];\n const black = { 12: 256, 14: 1024, 16: 4096 }[p.adcBitDepth], n = p.readNoise;\n const built = bodyAssets(version, \"photivra-sensor-profile-\" + p.id, p.maximumIso, 1, 1, { bitDepth: p.adcBitDepth, blackLevelCode: black,\n saturationElectrons: p.fullWellElectrons, maximumAnalogGain: p.maximumAnalogGain, lowGainReadNoise: n.lowGainElectrons * readNoiseScale, highGainReadNoise: n.highGainElectrons * readNoiseScale,\n highGainFromIso: n.highGainFromIso ?? Infinity, highGainMaximumCharge: (p.highGainFullWellElectrons ?? p.fullWellElectrons) * (1 + 1e-12),\n prnu: p.fixedPattern.prnu, dsnu: p.fixedPattern.dsnu, hotFraction: p.fixedPattern.hotPixelFraction, hotMultiplier: p.fixedPattern.hotPixelMultiplier,\n rowRms: p.fixedPattern.rowElectrons, columnRms: p.fixedPattern.columnElectrons, instanceSeed: (0x9e3779b9 ^ (p.pixelsWide * 8191 + p.pixelsHigh)) >>> 0 },\n p.readoutSeconds, .5, .01, false, false, { isoValues: ISO_MARKINGS.filter((iso) => iso >= p.baseIso && iso <= p.maximumIso), evidence,\n limitations: [\"Sensor profile \" + p.name + \" (#554): published, derived and design values (docs/SENSOR_PROFILES.md); not a calibration of any camera.\"] });\n // Only the sensor assets are taken; the placeholders above (release, stabilization …) are the body's own.\n return { ...body, exposure: built.exposure, iso: built.iso, signalChain: built.signalChain, readout: built.readout, readoutTiming: built.readoutTiming,\n rollingReadoutTiming: built.rollingReadoutTiming, fixedPatternNoise: built.fixedPatternNoise, flashSync: built.flashSync };\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SensorProfile } from "./sensor-profile-body.js";
|
|
2
|
+
export type { SensorProfile, SensorProfileValueKind } from "./sensor-profile-body.js";
|
|
3
|
+
/** The catalogue, in the issue's order. Frozen. */
|
|
4
|
+
export declare const SENSOR_PROFILES: readonly SensorProfile[];
|
|
5
|
+
/** The profile ids, in catalogue order. */
|
|
6
|
+
export declare const SENSOR_PROFILE_IDS: readonly string[];
|
|
7
|
+
/** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */
|
|
8
|
+
export declare function resolveSensorProfile(id: unknown): SensorProfile;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Sensor profile catalogue (#554): eight representative, unnamed sensors whose values come from manufacturer-published
|
|
4
|
+
* specifications and openly published papers, or are labelled design values inside the range those sources support.
|
|
5
|
+
* Profiles are named by format, resolution, architecture and era; the market tag is a label only and enters no equation.
|
|
6
|
+
*
|
|
7
|
+
* A capture takes a profile record, `simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`; this module is data only
|
|
8
|
+
* and is never imported by the capture, so an application's capture worker does not bundle it (the record is checked by
|
|
9
|
+
* `parseSensorProfile` in `sensor-profile-body.ts`).
|
|
10
|
+
* @see docs/SENSOR_PROFILES.md for every value's source or design basis.
|
|
11
|
+
*/
|
|
12
|
+
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
13
|
+
const BSI = { prnu: .004, dsnu: .15, hotPixelFraction: 5e-5, hotPixelMultiplier: 30, rowElectrons: .15, columnElectrons: .1 };
|
|
14
|
+
const FSI = { prnu: .006, dsnu: .2, hotPixelFraction: 1e-4, hotPixelMultiplier: 40, rowElectrons: .3, columnElectrons: .2 };
|
|
15
|
+
const sourced = (key, ...fields) => Object.fromEntries(fields.map((f) => [f, "published " + key]));
|
|
16
|
+
/** The catalogue, in the issue's order. Frozen. */
|
|
17
|
+
export const SENSOR_PROFILES = /* @__PURE__ */ Object.freeze([
|
|
18
|
+
{ id: "full-frame-24mp-bsi-2020s", name: "Full frame 24 MP BSI, 2020s", marketTag: "consumer", architecture: "back-illuminated",
|
|
19
|
+
sensorWidthMm: 36.07, sensorHeightMm: 24.01, pixelsWide: 6072, pixelsHigh: 4042, pixelPitchMicrometers: 5.94, peakQuantumEfficiency: .8,
|
|
20
|
+
fullWellElectrons: 100000, readNoise: { lowGainElectrons: 6.4, highGainElectrons: 1.1, highGainFromIso: 500 }, highGainFullWellElectrons: 20000,
|
|
21
|
+
adcBitDepth: 14, baseIso: 100, maximumIso: 51200, maximumAnalogGain: 64, readoutSeconds: .05, fixedPattern: BSI,
|
|
22
|
+
goodFor: ["landscape", "portrait", "travel"], description: "Large pixels and a deep full well for wide dynamic range at base ISO; a moderate readout shows some rolling-shutter skew.",
|
|
23
|
+
fixedLenses: null, provenance: { ...sourced("S1", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise", "adcBitDepth"),
|
|
24
|
+
sensorSize: "derived", architecture: "published S1", highGainFromIso: "derived S1", highGainFullWellElectrons: "derived", baseIso: "derived" } },
|
|
25
|
+
{ id: "full-frame-45mp-stacked-2020s", name: "Full frame 45 MP stacked, 2020s", marketTag: "professional", architecture: "stacked",
|
|
26
|
+
sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 8256, pixelsHigh: 5504, pixelPitchMicrometers: 4.36, peakQuantumEfficiency: .8,
|
|
27
|
+
fullWellElectrons: 69000, readNoise: { lowGainElectrons: 3.5, highGainElectrons: 1.5, highGainFromIso: 250 }, highGainFullWellElectrons: 22080,
|
|
28
|
+
adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .005, fixedPattern: BSI,
|
|
29
|
+
goodFor: ["landscape", "studio", "wildlife"], description: "High resolution with a fast stacked readout: fine detail for landscape and studio work, little rolling-shutter skew for action.",
|
|
30
|
+
fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", fullWellElectrons: "design (S2 scaled by pixel area)", readNoise: "design (S2)", highGainFromIso: "design (S2)",
|
|
31
|
+
highGainFullWellElectrons: "derived", baseIso: "derived" } },
|
|
32
|
+
{ id: "full-frame-24mp-stacked-sports-2020s", name: "Full frame 24 MP stacked (sports), 2020s", marketTag: "professional", architecture: "stacked",
|
|
33
|
+
sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 6000, pixelsHigh: 4000, pixelPitchMicrometers: 6, peakQuantumEfficiency: .8,
|
|
34
|
+
fullWellElectrons: 80000, readNoise: { lowGainElectrons: 3, highGainElectrons: 1, highGainFromIso: 400 }, highGainFullWellElectrons: 25000,
|
|
35
|
+
adcBitDepth: 14, baseIso: 125, maximumIso: 102400, maximumAnalogGain: 64, readoutSeconds: .0025, fixedPattern: BSI,
|
|
36
|
+
goodFor: ["sports", "wildlife", "journalism"], description: "The fastest readout and low high-gain read noise: almost no rolling-shutter skew and clean high ISO, at a moderate resolution.",
|
|
37
|
+
fixedLenses: null, provenance: { pixelPitchMicrometers: "derived", highGainFullWellElectrons: "derived", baseIso: "derived" } },
|
|
38
|
+
{ id: "full-frame-24mp-fsi-2010s", name: "Full frame 24 MP front-illuminated, 2010s", marketTag: "professional", architecture: "front-illuminated",
|
|
39
|
+
sensorWidthMm: 36.01, sensorHeightMm: 24.07, pixelsWide: 6032, pixelsHigh: 4032, pixelPitchMicrometers: 5.97, peakQuantumEfficiency: .53,
|
|
40
|
+
fullWellElectrons: 76000, readNoise: { lowGainElectrons: 4, highGainElectrons: 4, highGainFromIso: null }, highGainFullWellElectrons: null,
|
|
41
|
+
adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .08, fixedPattern: FSI,
|
|
42
|
+
goodFor: ["portrait", "landscape", "journalism"], description: "An older professional sensor: lower QE and one conversion gain, so a modern consumer BSI sensor beats it at high ISO; slow readout.",
|
|
43
|
+
fixedLenses: null, provenance: { ...sourced("S3", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "adcBitDepth"),
|
|
44
|
+
sensorSize: "derived", architecture: "derived S4", baseIso: "derived" } },
|
|
45
|
+
{ id: "aps-c-24mp-fsi-2020s", name: "APS-C 24 MP front-illuminated, 2020s", marketTag: "consumer", architecture: "front-illuminated",
|
|
46
|
+
sensorWidthMm: 23.55, sensorHeightMm: 15.73, pixelsWide: 6024, pixelsHigh: 4024, pixelPitchMicrometers: 3.91, peakQuantumEfficiency: .55,
|
|
47
|
+
fullWellElectrons: 36000, readNoise: { lowGainElectrons: 2.7, highGainElectrons: 2.7, highGainFromIso: null }, highGainFullWellElectrons: null,
|
|
48
|
+
adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .06, fixedPattern: FSI,
|
|
49
|
+
goodFor: ["travel", "family", "learning"], description: "A smaller, front-illuminated sensor: less light per pixel, a slower readout and more rolling-shutter skew.",
|
|
50
|
+
fixedLenses: null, provenance: { ...sourced("S5", "pixels", "pixelPitchMicrometers", "architecture", "fullWellElectrons", "readNoise", "adcBitDepth"), sensorSize: "derived", baseIso: "derived" } },
|
|
51
|
+
{ id: "aps-c-26mp-bsi-2020s", name: "APS-C 26 MP BSI, 2020s", marketTag: "professional", architecture: "back-illuminated",
|
|
52
|
+
sensorWidthMm: 23.49, sensorHeightMm: 15.7, pixelsWide: 6248, pixelsHigh: 4176, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
|
|
53
|
+
fullWellElectrons: 50000, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16000,
|
|
54
|
+
adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .025, fixedPattern: BSI,
|
|
55
|
+
goodFor: ["wildlife", "sports", "travel"], description: "A BSI APS-C sensor with dual conversion gain whose readout beats the consumer full-frame profile's.",
|
|
56
|
+
fixedLenses: null, provenance: { ...sourced("S6", "pixels", "pixelPitchMicrometers", "peakQuantumEfficiency", "fullWellElectrons", "readNoise"), sensorSize: "derived",
|
|
57
|
+
architecture: "published S6", highGainFromIso: "design (S7)", highGainFullWellElectrons: "derived", baseIso: "derived" } },
|
|
58
|
+
{ id: "medium-format-100mp-bsi-16bit", name: "Medium format 100 MP BSI, 16-bit", marketTag: "professional", architecture: "back-illuminated",
|
|
59
|
+
sensorWidthMm: 43.856, sensorHeightMm: 32.9, pixelsWide: 11656, pixelsHigh: 8750, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,
|
|
60
|
+
fullWellElectrons: 50300, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16096,
|
|
61
|
+
adcBitDepth: 16, baseIso: 80, maximumIso: 12800, maximumAnalogGain: 64, readoutSeconds: .25, fixedPattern: BSI,
|
|
62
|
+
goodFor: ["studio", "landscape", "portrait"], description: "A large sensor with 100 MP and a 16-bit RAW; a slow readout makes its electronic shutter unsuited to motion.",
|
|
63
|
+
fixedLenses: null, provenance: { ...sourced("S7", "pixels", "pixelPitchMicrometers", "sensorSize", "fullWellElectrons", "readNoise", "adcBitDepth"), architecture: "published S8",
|
|
64
|
+
highGainFromIso: "derived S7", highGainFullWellElectrons: "derived", baseIso: "derived" } },
|
|
65
|
+
{ id: "phone-50mp-stacked", name: "Phone, large, 50 MP stacked", marketTag: "consumer", architecture: "stacked",
|
|
66
|
+
sensorWidthMm: 9.844, sensorHeightMm: 7.431, pixelsWide: 8192, pixelsHigh: 6144, pixelPitchMicrometers: 1.197, peakQuantumEfficiency: .75,
|
|
67
|
+
fullWellElectrons: 8600, readNoise: { lowGainElectrons: 3, highGainElectrons: 1.4, highGainFromIso: 160 }, highGainFullWellElectrons: 2150,
|
|
68
|
+
adcBitDepth: 12, baseIso: 40, maximumIso: 6400, maximumAnalogGain: 64, readoutSeconds: 1 / 30,
|
|
69
|
+
fixedPattern: { prnu: .008, dsnu: .3, hotPixelFraction: 2e-4, hotPixelMultiplier: 50, rowElectrons: .2, columnElectrons: .2 },
|
|
70
|
+
goodFor: ["everyday", "travel", "family"], description: "A large phone sensor: tiny pixels with a small full well, so less dynamic range and more noise per pixel; simulated as a single frame (no multi-frame stacking).",
|
|
71
|
+
fixedLenses: "phone", provenance: { ...sourced("S9", "pixels", "pixelPitchMicrometers", "sensorSize", "architecture", "adcBitDepth"), fullWellElectrons: "design (S10 scaled by pixel area)",
|
|
72
|
+
readNoise: "high gain published S10, low gain design", highGainFullWellElectrons: "derived", readoutSeconds: "derived S9 (1 / full-resolution frame rate)",
|
|
73
|
+
fixedPattern: "PRNU, row and column published S10; the rest design", baseIso: "derived" } }
|
|
74
|
+
].map((p) => Object.freeze(p)));
|
|
75
|
+
/** The profile ids, in catalogue order. */
|
|
76
|
+
export const SENSOR_PROFILE_IDS = /* @__PURE__ */ Object.freeze(SENSOR_PROFILES.map((p) => p.id));
|
|
77
|
+
/** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */
|
|
78
|
+
export function resolveSensorProfile(id) {
|
|
79
|
+
return SENSOR_PROFILES.find((p) => p.id === id) ?? fail("resolveSensorProfile: the id must be one of " + SENSOR_PROFILE_IDS.join(", ") + ".");
|
|
80
|
+
}
|
|
81
|
+
function fail(message) { throw new InvalidConfigurationError(message); }
|
|
82
|
+
//# sourceMappingURL=sensor-profiles.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensor-profiles.js","sourceRoot":"","sources":["../../src/equipment/sensor-profiles.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Sensor profile catalogue (#554): eight representative, unnamed sensors whose values come from manufacturer-published\n * specifications and openly published papers, or are labelled design values inside the range those sources support.\n * Profiles are named by format, resolution, architecture and era; the market tag is a label only and enters no equation.\n *\n * A capture takes a profile record, `simulateCapture({ sensor: { profile: SENSOR_PROFILES[i] } })`; this module is data only\n * and is never imported by the capture, so an application's capture worker does not bundle it (the record is checked by\n * `parseSensorProfile` in `sensor-profile-body.ts`).\n * @see docs/SENSOR_PROFILES.md for every value's source or design basis.\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport type { SensorProfile } from \"./sensor-profile-body.js\";\n\nexport type { SensorProfile, SensorProfileValueKind } from \"./sensor-profile-body.js\";\n\nconst BSI = { prnu: .004, dsnu: .15, hotPixelFraction: 5e-5, hotPixelMultiplier: 30, rowElectrons: .15, columnElectrons: .1 };\nconst FSI = { prnu: .006, dsnu: .2, hotPixelFraction: 1e-4, hotPixelMultiplier: 40, rowElectrons: .3, columnElectrons: .2 };\nconst sourced = (key: string, ...fields: string[]): Record<string, string> => Object.fromEntries(fields.map((f) => [f, \"published \" + key]));\n\n/** The catalogue, in the issue's order. Frozen. */\nexport const SENSOR_PROFILES: readonly SensorProfile[] = /* @__PURE__ */ Object.freeze(([\n { id: \"full-frame-24mp-bsi-2020s\", name: \"Full frame 24 MP BSI, 2020s\", marketTag: \"consumer\", architecture: \"back-illuminated\",\n sensorWidthMm: 36.07, sensorHeightMm: 24.01, pixelsWide: 6072, pixelsHigh: 4042, pixelPitchMicrometers: 5.94, peakQuantumEfficiency: .8,\n fullWellElectrons: 100000, readNoise: { lowGainElectrons: 6.4, highGainElectrons: 1.1, highGainFromIso: 500 }, highGainFullWellElectrons: 20000,\n adcBitDepth: 14, baseIso: 100, maximumIso: 51200, maximumAnalogGain: 64, readoutSeconds: .05, fixedPattern: BSI,\n goodFor: [\"landscape\", \"portrait\", \"travel\"], description: \"Large pixels and a deep full well for wide dynamic range at base ISO; a moderate readout shows some rolling-shutter skew.\",\n fixedLenses: null, provenance: { ...sourced(\"S1\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"),\n sensorSize: \"derived\", architecture: \"published S1\", highGainFromIso: \"derived S1\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-45mp-stacked-2020s\", name: \"Full frame 45 MP stacked, 2020s\", marketTag: \"professional\", architecture: \"stacked\",\n sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 8256, pixelsHigh: 5504, pixelPitchMicrometers: 4.36, peakQuantumEfficiency: .8,\n fullWellElectrons: 69000, readNoise: { lowGainElectrons: 3.5, highGainElectrons: 1.5, highGainFromIso: 250 }, highGainFullWellElectrons: 22080,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .005, fixedPattern: BSI,\n goodFor: [\"landscape\", \"studio\", \"wildlife\"], description: \"High resolution with a fast stacked readout: fine detail for landscape and studio work, little rolling-shutter skew for action.\",\n fixedLenses: null, provenance: { pixelPitchMicrometers: \"derived\", fullWellElectrons: \"design (S2 scaled by pixel area)\", readNoise: \"design (S2)\", highGainFromIso: \"design (S2)\",\n highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-24mp-stacked-sports-2020s\", name: \"Full frame 24 MP stacked (sports), 2020s\", marketTag: \"professional\", architecture: \"stacked\",\n sensorWidthMm: 36, sensorHeightMm: 24, pixelsWide: 6000, pixelsHigh: 4000, pixelPitchMicrometers: 6, peakQuantumEfficiency: .8,\n fullWellElectrons: 80000, readNoise: { lowGainElectrons: 3, highGainElectrons: 1, highGainFromIso: 400 }, highGainFullWellElectrons: 25000,\n adcBitDepth: 14, baseIso: 125, maximumIso: 102400, maximumAnalogGain: 64, readoutSeconds: .0025, fixedPattern: BSI,\n goodFor: [\"sports\", \"wildlife\", \"journalism\"], description: \"The fastest readout and low high-gain read noise: almost no rolling-shutter skew and clean high ISO, at a moderate resolution.\",\n fixedLenses: null, provenance: { pixelPitchMicrometers: \"derived\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"full-frame-24mp-fsi-2010s\", name: \"Full frame 24 MP front-illuminated, 2010s\", marketTag: \"professional\", architecture: \"front-illuminated\",\n sensorWidthMm: 36.01, sensorHeightMm: 24.07, pixelsWide: 6032, pixelsHigh: 4032, pixelPitchMicrometers: 5.97, peakQuantumEfficiency: .53,\n fullWellElectrons: 76000, readNoise: { lowGainElectrons: 4, highGainElectrons: 4, highGainFromIso: null }, highGainFullWellElectrons: null,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .08, fixedPattern: FSI,\n goodFor: [\"portrait\", \"landscape\", \"journalism\"], description: \"An older professional sensor: lower QE and one conversion gain, so a modern consumer BSI sensor beats it at high ISO; slow readout.\",\n fixedLenses: null, provenance: { ...sourced(\"S3\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"adcBitDepth\"),\n sensorSize: \"derived\", architecture: \"derived S4\", baseIso: \"derived\" } },\n { id: \"aps-c-24mp-fsi-2020s\", name: \"APS-C 24 MP front-illuminated, 2020s\", marketTag: \"consumer\", architecture: \"front-illuminated\",\n sensorWidthMm: 23.55, sensorHeightMm: 15.73, pixelsWide: 6024, pixelsHigh: 4024, pixelPitchMicrometers: 3.91, peakQuantumEfficiency: .55,\n fullWellElectrons: 36000, readNoise: { lowGainElectrons: 2.7, highGainElectrons: 2.7, highGainFromIso: null }, highGainFullWellElectrons: null,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .06, fixedPattern: FSI,\n goodFor: [\"travel\", \"family\", \"learning\"], description: \"A smaller, front-illuminated sensor: less light per pixel, a slower readout and more rolling-shutter skew.\",\n fixedLenses: null, provenance: { ...sourced(\"S5\", \"pixels\", \"pixelPitchMicrometers\", \"architecture\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"), sensorSize: \"derived\", baseIso: \"derived\" } },\n { id: \"aps-c-26mp-bsi-2020s\", name: \"APS-C 26 MP BSI, 2020s\", marketTag: \"professional\", architecture: \"back-illuminated\",\n sensorWidthMm: 23.49, sensorHeightMm: 15.7, pixelsWide: 6248, pixelsHigh: 4176, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,\n fullWellElectrons: 50000, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16000,\n adcBitDepth: 14, baseIso: 80, maximumIso: 25600, maximumAnalogGain: 64, readoutSeconds: .025, fixedPattern: BSI,\n goodFor: [\"wildlife\", \"sports\", \"travel\"], description: \"A BSI APS-C sensor with dual conversion gain whose readout beats the consumer full-frame profile's.\",\n fixedLenses: null, provenance: { ...sourced(\"S6\", \"pixels\", \"pixelPitchMicrometers\", \"peakQuantumEfficiency\", \"fullWellElectrons\", \"readNoise\"), sensorSize: \"derived\",\n architecture: \"published S6\", highGainFromIso: \"design (S7)\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"medium-format-100mp-bsi-16bit\", name: \"Medium format 100 MP BSI, 16-bit\", marketTag: \"professional\", architecture: \"back-illuminated\",\n sensorWidthMm: 43.856, sensorHeightMm: 32.9, pixelsWide: 11656, pixelsHigh: 8750, pixelPitchMicrometers: 3.76, peakQuantumEfficiency: .8,\n fullWellElectrons: 50300, readNoise: { lowGainElectrons: 3.3, highGainElectrons: 1, highGainFromIso: 250 }, highGainFullWellElectrons: 16096,\n adcBitDepth: 16, baseIso: 80, maximumIso: 12800, maximumAnalogGain: 64, readoutSeconds: .25, fixedPattern: BSI,\n goodFor: [\"studio\", \"landscape\", \"portrait\"], description: \"A large sensor with 100 MP and a 16-bit RAW; a slow readout makes its electronic shutter unsuited to motion.\",\n fixedLenses: null, provenance: { ...sourced(\"S7\", \"pixels\", \"pixelPitchMicrometers\", \"sensorSize\", \"fullWellElectrons\", \"readNoise\", \"adcBitDepth\"), architecture: \"published S8\",\n highGainFromIso: \"derived S7\", highGainFullWellElectrons: \"derived\", baseIso: \"derived\" } },\n { id: \"phone-50mp-stacked\", name: \"Phone, large, 50 MP stacked\", marketTag: \"consumer\", architecture: \"stacked\",\n sensorWidthMm: 9.844, sensorHeightMm: 7.431, pixelsWide: 8192, pixelsHigh: 6144, pixelPitchMicrometers: 1.197, peakQuantumEfficiency: .75,\n fullWellElectrons: 8600, readNoise: { lowGainElectrons: 3, highGainElectrons: 1.4, highGainFromIso: 160 }, highGainFullWellElectrons: 2150,\n adcBitDepth: 12, baseIso: 40, maximumIso: 6400, maximumAnalogGain: 64, readoutSeconds: 1 / 30,\n fixedPattern: { prnu: .008, dsnu: .3, hotPixelFraction: 2e-4, hotPixelMultiplier: 50, rowElectrons: .2, columnElectrons: .2 },\n goodFor: [\"everyday\", \"travel\", \"family\"], description: \"A large phone sensor: tiny pixels with a small full well, so less dynamic range and more noise per pixel; simulated as a single frame (no multi-frame stacking).\",\n fixedLenses: \"phone\", provenance: { ...sourced(\"S9\", \"pixels\", \"pixelPitchMicrometers\", \"sensorSize\", \"architecture\", \"adcBitDepth\"), fullWellElectrons: \"design (S10 scaled by pixel area)\",\n readNoise: \"high gain published S10, low gain design\", highGainFullWellElectrons: \"derived\", readoutSeconds: \"derived S9 (1 / full-resolution frame rate)\",\n fixedPattern: \"PRNU, row and column published S10; the rest design\", baseIso: \"derived\" } }\n] satisfies SensorProfile[]).map((p) => Object.freeze(p)));\n\n/** The profile ids, in catalogue order. */\nexport const SENSOR_PROFILE_IDS: readonly string[] = /* @__PURE__ */ Object.freeze(SENSOR_PROFILES.map((p) => p.id));\n\n/** The profile record with this id (for `sensor.profile`); a clear error listing the ids otherwise. */\nexport function resolveSensorProfile(id: unknown): SensorProfile {\n return SENSOR_PROFILES.find((p) => p.id === id) ?? fail(\"resolveSensorProfile: the id must be one of \" + SENSOR_PROFILE_IDS.join(\", \") + \".\");\n}\nfunction fail(message: string): never { throw new InvalidConfigurationError(message); }\n"]}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type IlluminationVignettingProfile } from "../optics/illumination-vignetting.js";
|
|
2
2
|
import { type ResolveLensSampledPsfInput } from "../optics/lens-psf-profile.js";
|
|
3
3
|
import type { DisplayViewScene } from "./expected-signal.js";
|
|
4
|
-
/** Fewest grid cells between neighboring profile field nodes. */
|
|
5
|
-
export declare const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS =
|
|
4
|
+
/** Fewest grid cells between neighboring profile field nodes (10 since #589: with 4, 0.4 % of quick frames failed the run-time check). */
|
|
5
|
+
export declare const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 10;
|
|
6
6
|
/** Largest move of a node kernel's intensity centroid across one grid cell, in sites (sets finer grids where lateral color moves the PSF). */
|
|
7
7
|
export declare const CATALOGUE_FAST_GRID_CENTROID_STEP_SITES = 0.05;
|
|
8
8
|
/**
|
|
@@ -18,9 +18,11 @@
|
|
|
18
18
|
* The grid subdivides each interval between the profile's field nodes into at least GRID_MINIMUM_SUBDIVISIONS cells, and into
|
|
19
19
|
* more where the node kernels' intensity centroids move by more than GRID_CENTROID_STEP_SITES sites across the interval (lateral
|
|
20
20
|
* colour moves the PSF across the frame; a blend of two shifted kernels is not the shifted kernel). Since the blend is linear,
|
|
21
|
-
* the channel response
|
|
22
|
-
*
|
|
23
|
-
*
|
|
21
|
+
* the channel response folds into the grid kernels once per grid point and node, and each tap's signal is integrated from its
|
|
22
|
+
* material's spectrum when the site is read (2.21.0; no material is folded into a kernel):
|
|
23
|
+
* μ̃_c(q) = Σ_k F(s_k)·Σ_λ L_{m(s_k)}(λ)·Σ_g b_g(q)·C_{g,c}(k, λ), C_{g,c}(k, λ) = A_c(λ)·K_{g,λ}(k),
|
|
24
|
+
* the blend taken along y once per grid column and native row, so a site costs (2R + 1)² taps of N nodes each instead of
|
|
25
|
+
* (2R + 1)² scene queries per spectral node. Its error against the exact method is
|
|
24
26
|
* bounded by ‖K̃ − K‖₁ times the brightest tap's signal; tests measure both (test/simulate-capture-lens-catalogue-fast.test.ts).
|
|
25
27
|
*
|
|
26
28
|
* The scene points. The mapping is the environment's (`undistortedImagePoint`, `lensProjectionDirectionValue`, the planar
|
|
@@ -32,9 +34,9 @@ import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
|
32
34
|
import { insideRadialImageCircle, lensProjectionDirectionValue } from "../camera/lens-projection.js";
|
|
33
35
|
import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
|
|
34
36
|
import { prepareIlluminationVignettingThroughput } from "../optics/illumination-vignetting.js";
|
|
35
|
-
import { resolveLensSampledPsfKernel } from "../optics/lens-psf-profile.js";
|
|
36
|
-
/** Fewest grid cells between neighboring profile field nodes. */
|
|
37
|
-
export const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS =
|
|
37
|
+
import { kernelCentroid, resolveLensSampledPsfKernel } from "../optics/lens-psf-profile.js";
|
|
38
|
+
/** Fewest grid cells between neighboring profile field nodes (10 since #589: with 4, 0.4 % of quick frames failed the run-time check). */
|
|
39
|
+
export const CATALOGUE_FAST_GRID_MINIMUM_SUBDIVISIONS = 10;
|
|
38
40
|
/** Largest move of a node kernel's intensity centroid across one grid cell, in sites (sets finer grids where lateral color moves the PSF). */
|
|
39
41
|
export const CATALOGUE_FAST_GRID_CENTROID_STEP_SITES = 0.05;
|
|
40
42
|
/**
|
|
@@ -45,17 +47,6 @@ export const CATALOGUE_FAST_VERIFICATION_TOLERANCE = 0.005;
|
|
|
45
47
|
/** Largest cache window, in lattice points; a larger window is evaluated without caching. */
|
|
46
48
|
const WINDOW_LIMIT = 1 << 22;
|
|
47
49
|
const NEWTON_ITERATIONS = 60;
|
|
48
|
-
/** A kernel's intensity centroid, in samples. */
|
|
49
|
-
function centroid(values, width) {
|
|
50
|
-
let sum = 0, x = 0, y = 0;
|
|
51
|
-
for (let i = 0; i < values.length; i++) {
|
|
52
|
-
const v = values[i];
|
|
53
|
-
sum += v;
|
|
54
|
-
x += v * (i % width);
|
|
55
|
-
y += v * Math.floor(i / width);
|
|
56
|
-
}
|
|
57
|
-
return { x: x / sum, y: y / sum };
|
|
58
|
-
}
|
|
59
50
|
/**
|
|
60
51
|
* The grid axes: each interval between neighboring profile field nodes split into max(GRID_MINIMUM_SUBDIVISIONS,
|
|
61
52
|
* ⌈Δc / GRID_CENTROID_STEP_SITES⌉) equal cells, Δc the largest centroid move between the interval's two nodes over the other
|
|
@@ -66,7 +57,7 @@ export function catalogueFastGridAxes(psf) {
|
|
|
66
57
|
const centroids = new Map();
|
|
67
58
|
for (const node of profile.nodes) {
|
|
68
59
|
const c = node.coordinate;
|
|
69
|
-
centroids.set(c.fieldXmm + ":" + c.fieldYmm + ":" + c.wavelengthNm,
|
|
60
|
+
centroids.set(c.fieldXmm + ":" + c.fieldYmm + ":" + c.wavelengthNm, kernelCentroid(node.kernel));
|
|
70
61
|
}
|
|
71
62
|
const at = (x, y, nm) => {
|
|
72
63
|
const c = centroids.get(x + ":" + y + ":" + nm);
|
|
@@ -122,18 +113,12 @@ export function createCatalogueLensSignal(given) {
|
|
|
122
113
|
throw new InvalidConfigurationError("lens.catalogue fast: the PSF profile's kernels differ in size.");
|
|
123
114
|
// Lattice reach of the taps around a site: x from −cx … kw − 1 − cx (as c_x − k_x), y from −cy … kh − 1 − cy.
|
|
124
115
|
const reachLeft = kw - 1 - cx, reachRight = cx, reachUp = cy, reachDown = kh - 1 - cy;
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
const
|
|
128
|
-
scene.materials.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
materialClass[index] = 1 + spectral.length;
|
|
132
|
-
spectral.push(m.radiance);
|
|
133
|
-
} });
|
|
134
|
-
const classes = 1 + spectral.length, classRadiance = [scene.whiteRadiance, ...spectral];
|
|
135
|
-
const tableSize = 3 * classes * taps;
|
|
136
|
-
// W_{c,m}(k) = Σ_λ A_c(λ)·L_m(λ)·K_λ(k) for the kernels of one field point.
|
|
116
|
+
// Each material's spectral radiance at the nodes (a neutral one: its reflectance times the white radiance). Only the spectra are
|
|
117
|
+
// kept: a site's signal is integrated from its taps' material spectra when it is read, so no material is folded into a kernel.
|
|
118
|
+
const M = scene.materials.length;
|
|
119
|
+
const spectra = scene.materials.map((m) => ("neutral" in m ? Float64Array.from(scene.whiteRadiance, (w) => m.neutral * w) : Float64Array.from(m.radiance)));
|
|
120
|
+
const tableSize = 3 * taps * nodes;
|
|
121
|
+
// A_c(λ_n)·K_n(k) for the kernels of one field point, at ((c·taps + k)·nodes + n): the channel response per node, no material.
|
|
137
122
|
const foldKernels = (fieldXmm, fieldYmm) => {
|
|
138
123
|
const table = new Float64Array(tableSize);
|
|
139
124
|
for (let n = 0; n < nodes; n++) {
|
|
@@ -142,15 +127,8 @@ export function createCatalogueLensSignal(given) {
|
|
|
142
127
|
throw new InvalidConfigurationError("lens.catalogue fast: a resolved kernel differs in size.");
|
|
143
128
|
for (let c = 0; c < 3; c++) {
|
|
144
129
|
const a = A[c][n];
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
for (let cl = 0; cl < classes; cl++) {
|
|
148
|
-
const weight = a * classRadiance[cl][n], base = (c * classes + cl) * taps;
|
|
149
|
-
if (weight === 0)
|
|
150
|
-
continue;
|
|
151
|
-
for (let k = 0; k < taps; k++)
|
|
152
|
-
table[base + k] = table[base + k] + weight * kernel[k];
|
|
153
|
-
}
|
|
130
|
+
for (let k = 0; k < taps; k++)
|
|
131
|
+
table[(c * taps + k) * nodes + n] = a * kernel[k];
|
|
154
132
|
}
|
|
155
133
|
}
|
|
156
134
|
return table;
|
|
@@ -279,40 +257,39 @@ export function createCatalogueLensSignal(given) {
|
|
|
279
257
|
winFactor[q * w + p] = lf;
|
|
280
258
|
}
|
|
281
259
|
};
|
|
282
|
-
|
|
283
|
-
|
|
260
|
+
// A site's kernel is the bilinear blend of its cell's four grid kernels. Blended along y first, once per grid column and native
|
|
261
|
+
// row (every site of a row has the same fy): R_i(y) = (1 − fy)·T(i, j) + fy·T(i, j + 1), kept for the row the sites are on.
|
|
262
|
+
const rowTables = [], rowOf = new Int32Array(GX).fill(-1);
|
|
263
|
+
const rowTable = (i, y) => {
|
|
264
|
+
let table = rowTables[i];
|
|
265
|
+
if (!table)
|
|
266
|
+
table = rowTables[i] = new Float64Array(tableSize);
|
|
267
|
+
if (rowOf[i] !== y) {
|
|
268
|
+
const j = cellY[y], fy = weightY[y], a = tableAt(i, j), b = tableAt(i, j + 1);
|
|
269
|
+
for (let e = 0; e < tableSize; e++)
|
|
270
|
+
table[e] = (1 - fy) * a[e] + fy * b[e];
|
|
271
|
+
rowOf[i] = y;
|
|
272
|
+
}
|
|
273
|
+
return table;
|
|
274
|
+
};
|
|
284
275
|
const site = (x, y) => {
|
|
285
276
|
const c = channelAt(x, y);
|
|
286
|
-
let
|
|
277
|
+
let r0, r1, fx;
|
|
287
278
|
if (input.exactKernels) {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
b00 = 1;
|
|
291
|
-
b10 = b01 = b11 = 0;
|
|
279
|
+
r0 = r1 = foldKernels((x + 0.5 - W / 2) * mmPerSite, -(y + 0.5 - H / 2) * mmPerSite);
|
|
280
|
+
fx = 0;
|
|
292
281
|
}
|
|
293
282
|
else {
|
|
294
|
-
const i = cellX[x]
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
cellJ = j;
|
|
299
|
-
c00 = tableAt(i, j);
|
|
300
|
-
c10 = tableAt(i + 1, j);
|
|
301
|
-
c01 = tableAt(i, j + 1);
|
|
302
|
-
c11 = tableAt(i + 1, j + 1);
|
|
303
|
-
}
|
|
304
|
-
t00 = c00;
|
|
305
|
-
t10 = c10;
|
|
306
|
-
t01 = c01;
|
|
307
|
-
t11 = c11;
|
|
308
|
-
b00 = (1 - fx) * (1 - fy);
|
|
309
|
-
b10 = fx * (1 - fy);
|
|
310
|
-
b01 = (1 - fx) * fy;
|
|
311
|
-
b11 = fx * fy;
|
|
283
|
+
const i = cellX[x];
|
|
284
|
+
r0 = rowTable(i, y);
|
|
285
|
+
r1 = rowTable(i + 1, y);
|
|
286
|
+
fx = weightX[x];
|
|
312
287
|
}
|
|
313
288
|
const inside = ww > 0 && x - reachLeft >= wx0 && y - reachUp >= wy0 && x + reachRight < wx0 + ww && y + reachDown < wy0 + wh;
|
|
314
|
-
const channelBase = c *
|
|
315
|
-
|
|
289
|
+
const channelBase = c * taps * nodes;
|
|
290
|
+
// Each tap's signal is integrated from its material's spectrum against the two row-blended kernels per node; the blend along x
|
|
291
|
+
// is linear, so it is applied to the two sums.
|
|
292
|
+
let s0 = 0, s1 = 0;
|
|
316
293
|
for (let ky = 0, k = 0; ky < kh; ky++) {
|
|
317
294
|
const q = y + ky - cy;
|
|
318
295
|
for (let kx = 0; kx < kw; kx++, k++) {
|
|
@@ -330,12 +307,22 @@ export function createCatalogueLensSignal(given) {
|
|
|
330
307
|
}
|
|
331
308
|
if (f === 0)
|
|
332
309
|
continue;
|
|
333
|
-
const
|
|
334
|
-
|
|
335
|
-
|
|
310
|
+
const L = spectra[m], at = channelBase + k * nodes;
|
|
311
|
+
let w0 = 0, w1 = 0;
|
|
312
|
+
if (fx === 0)
|
|
313
|
+
for (let n = 0; n < nodes; n++)
|
|
314
|
+
w0 += r0[at + n] * L[n];
|
|
315
|
+
else
|
|
316
|
+
for (let n = 0; n < nodes; n++) {
|
|
317
|
+
const l = L[n];
|
|
318
|
+
w0 += r0[at + n] * l;
|
|
319
|
+
w1 += r1[at + n] * l;
|
|
320
|
+
}
|
|
321
|
+
s0 += f * w0;
|
|
322
|
+
s1 += f * w1;
|
|
336
323
|
}
|
|
337
324
|
}
|
|
338
|
-
return
|
|
325
|
+
return fx === 0 ? s0 : (1 - fx) * s0 + fx * s1;
|
|
339
326
|
};
|
|
340
327
|
return {
|
|
341
328
|
window, site,
|
|
@@ -349,7 +336,7 @@ export function catalogueFastAssumptions(run) {
|
|
|
349
336
|
return [{ id: "raw.expectedSignal", value: { method: "tabulated-expected-electrons-catalogue-psf-field-grid", verificationSites: 4, tolerance,
|
|
350
337
|
verificationRelativeDifference: run.verificationRelativeDifference },
|
|
351
338
|
explanation: "Each native site's expected photo electrons are tabulated: the sum over the PSF taps of the field factor (vignetting × distortion area factor) times the " +
|
|
352
|
-
"grid-blended kernel
|
|
339
|
+
"grid-blended kernel per spectral node times the channel response, integrated with each tap's material spectrum (see fast.lens.catalogue). The sensor stage (dark charge, Poisson draws, read noise, ADC) " +
|
|
353
340
|
"is the native one with each site's own seeds. One site of each CFA phase ran the exact chain (the environment PSF quadrature): its readout was reproduced bit for bit and " +
|
|
354
341
|
"its electrons agreed with the fast ones within the tolerance (verificationRelativeDifference: the largest difference there)." },
|
|
355
342
|
{ id: "fast.lens.catalogue", value: { method: "field-grid-bilinear-psf", gridColumns: run.columns, gridRows: run.rows, gridPointsResolved: run.resolved,
|