@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
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type DisplayViewExpectedSignal, type DisplayViewRenderResult } from "./display-view.js";
|
|
2
|
+
import { type DisplayViewResult, type SimulateCaptureOptions, type SimulateDisplayViewOptions } from "./simulate-capture.js";
|
|
3
|
+
/**
|
|
4
|
+
* The display view of a capture request: the image the real full-resolution capture (`simulateCapture(options)`) would
|
|
5
|
+
* give after it is resized to `view.width` × `view.height` by area averaging in linear light, estimated at a cost that
|
|
6
|
+
* follows the display pixels, not the capture's. Optics (lens diffraction, the anti-aliasing filter, the pixel aperture)
|
|
7
|
+
* act at the real pixel pitch; the colour mosaic, demosaic and development are those of the real pixels; the noise is
|
|
8
|
+
* that of the average of the real pixels, demosaic correlation included; exposure, white balance and tone are the
|
|
9
|
+
* capture's. The result is labelled `kind: "display-view"`; it is not a capture.
|
|
10
|
+
* @see docs/DISPLAY_VIEW.md and docs/QUICK_START.md, "Display view".
|
|
11
|
+
*/
|
|
12
|
+
export declare function simulateDisplayView(options: SimulateCaptureOptions, view: SimulateDisplayViewOptions): Promise<DisplayViewResult>;
|
|
13
|
+
/**
|
|
14
|
+
* Internal (not exported from the package): the display view plus its noise-free expected linear values and noise standard
|
|
15
|
+
* deviations, for the validation scripts and tests.
|
|
16
|
+
*/
|
|
17
|
+
export declare function simulateDisplayViewWithInternals(options: SimulateCaptureOptions, view: SimulateDisplayViewOptions): Promise<{
|
|
18
|
+
result: DisplayViewResult;
|
|
19
|
+
internals: DisplayViewRenderResult;
|
|
20
|
+
signal: DisplayViewExpectedSignal;
|
|
21
|
+
}>;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* The public entry point of the display view (`simulateDisplayView`) and its renderer. `simulate-capture.ts` resolves the
|
|
4
|
+
* request (`runCapture`) and calls the renderer it is given, so a bundle that imports only `simulateCapture` or
|
|
5
|
+
* `simulateIdealReference` does not contain the display view (`display-view.ts`) or the portable evaluations it uses.
|
|
6
|
+
* The design and its error analysis are in docs/DISPLAY_VIEW.md.
|
|
7
|
+
*/
|
|
8
|
+
import { ENGINE_API_VERSION } from "../core/version.js";
|
|
9
|
+
import { resolvePortableColorTemperatureWhiteBalance } from "../color/white-balance.js";
|
|
10
|
+
import { resolveCaptureColorModel } from "../color/capture-color.js";
|
|
11
|
+
import { prepareSensorColorDevelopment } from "../color/sensor-color-development.js";
|
|
12
|
+
import { resolveLensPsfConvolutionKernels } from "../optics/lens-psf-kernels.js";
|
|
13
|
+
import { resolveProjectionDistanceMm } from "../motion/camera-rotation.js";
|
|
14
|
+
import { resolveSensorReadoutRegime } from "../sensor/raw-readout.js";
|
|
15
|
+
import { resolvePortableChartScene } from "./chart-scenes.js";
|
|
16
|
+
import { resolvePortableCaptureIlluminant, calculateIlluminantIrradianceScale } from "./scene-radiance.js";
|
|
17
|
+
import { ahdCandidateKernels, createDisplayViewExpectedSignal, renderDisplayView } from "./display-view.js";
|
|
18
|
+
import { runCapture, apertureNodeOffsets, bilinearPhaseProfiles, clockMs, fail, sceneIlluminantWhiteBalance, CAMERA_TO_XYZ, CHANNELS, DARK_CURRENT_ELECTRONS_PER_SECOND, FACADE_EVIDENCE, SIMULATE_CAPTURE_FACADE_VERSION } from "./simulate-capture.js";
|
|
19
|
+
/**
|
|
20
|
+
* The display view of a capture request: the image the real full-resolution capture (`simulateCapture(options)`) would
|
|
21
|
+
* give after it is resized to `view.width` × `view.height` by area averaging in linear light, estimated at a cost that
|
|
22
|
+
* follows the display pixels, not the capture's. Optics (lens diffraction, the anti-aliasing filter, the pixel aperture)
|
|
23
|
+
* act at the real pixel pitch; the colour mosaic, demosaic and development are those of the real pixels; the noise is
|
|
24
|
+
* that of the average of the real pixels, demosaic correlation included; exposure, white balance and tone are the
|
|
25
|
+
* capture's. The result is labelled `kind: "display-view"`; it is not a capture.
|
|
26
|
+
* @see docs/DISPLAY_VIEW.md and docs/QUICK_START.md, "Display view".
|
|
27
|
+
*/
|
|
28
|
+
export async function simulateDisplayView(options, view) {
|
|
29
|
+
return await runCapture(options, "display-view", view, (context) => renderDisplayViewResult(context));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Internal (not exported from the package): the display view plus its noise-free expected linear values and noise standard
|
|
33
|
+
* deviations, for the validation scripts and tests.
|
|
34
|
+
*/
|
|
35
|
+
export async function simulateDisplayViewWithInternals(options, view) {
|
|
36
|
+
const internals = {};
|
|
37
|
+
const result = await runCapture(options, "display-view", view, (context) => renderDisplayViewResult(context, internals));
|
|
38
|
+
return { result, internals: internals.value, signal: internals.signal };
|
|
39
|
+
}
|
|
40
|
+
/** AHD low-signal correction (display view): draws per flat-neighbourhood state, and the pixel noise (linear light) above which it applies. */
|
|
41
|
+
const DISPLAY_VIEW_AHD_CORRECTION_DRAWS = 2048, DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD = 0.01;
|
|
42
|
+
/** Renders the display view of a resolved request (the last step of `runCapture` for "display-view"). */
|
|
43
|
+
async function renderDisplayViewResult(context, viewInternals) {
|
|
44
|
+
const { ahd, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, chartReport, chartSize, diffraction, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl } = context;
|
|
45
|
+
const kelvinInput = context.kelvinWhiteBalanceInput;
|
|
46
|
+
const portableKelvinWhiteBalance = kelvinInput ? () => resolvePortableColorTemperatureWhiteBalance(kelvinInput) : undefined;
|
|
47
|
+
const viewStarted = clockMs(), v = parsedView;
|
|
48
|
+
// The engine's illuminant formulas (Planckian, CIE A, daylight locus) evaluated with the portable power and e^x − 1, so
|
|
49
|
+
// the view's radiances, irradiance scale and scene-illuminant white balance have the same bits in every JavaScript
|
|
50
|
+
// engine; they differ from the capture's own by at most a few ulps (docs/DISPLAY_VIEW.md, "Same bytes in every engine").
|
|
51
|
+
const viewIlluminant = resolvePortableCaptureIlluminant(illuminantOption), viewIrradianceScale = calculateIlluminantIrradianceScale(viewIlluminant.shape, illuminanceLux);
|
|
52
|
+
const viewProvider = buildProvider(viewIlluminant, viewIrradianceScale);
|
|
53
|
+
const viewWhiteBalanceState = whiteBalanceOption === "scene-illuminant" ? sceneIlluminantWhiteBalance(viewIlluminant) : portableKelvinWhiteBalance ? portableKelvinWhiteBalance() : whiteBalanceState;
|
|
54
|
+
// The chart's materials likewise (a step wedge's geometric levels use the portable power); same geometry and order.
|
|
55
|
+
const viewChart = chart ? resolvePortableChartScene({ scene: sceneOption, aspect: chartSize ? chartSize.heightM / chartSize.widthM : aspect }) : undefined;
|
|
56
|
+
if (chart && viewChart.materials.length !== chart.materials.length)
|
|
57
|
+
fail("simulateDisplayView: the chart's materials could not be resolved portably.");
|
|
58
|
+
const { electronsPerRadiance, wavelengths, relativePerElectron } = axisChannelResponse("simulateDisplayView");
|
|
59
|
+
const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0));
|
|
60
|
+
// Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.
|
|
61
|
+
const spots = antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];
|
|
62
|
+
const offsets = apertureNodeOffsets(boxSamples).flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,
|
|
63
|
+
weight: c.normalizedWeight / (boxSamples * boxSamples) })));
|
|
64
|
+
// Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).
|
|
65
|
+
let kernels;
|
|
66
|
+
if (diffraction) {
|
|
67
|
+
const radius = diffraction.kernelRadiusSamples;
|
|
68
|
+
kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {
|
|
69
|
+
if (!diffraction.profile.axes.wavelengthNm.includes(wavelengthNm))
|
|
70
|
+
fail("simulateDisplayView: a spectral node has no diffraction kernel.");
|
|
71
|
+
const k = resolveLensPsfConvolutionKernels({ schemaVersion: "0.1.0", psf: diffractionPsf.configuration.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],
|
|
72
|
+
grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1, kernelRadius: radius }).value;
|
|
73
|
+
if (!k.exactSiteSampling || k.kernelRadius !== radius)
|
|
74
|
+
fail("simulateDisplayView: diffraction kernels must sample the site pitch exactly.");
|
|
75
|
+
return Float64Array.from(k.kernels);
|
|
76
|
+
}) };
|
|
77
|
+
}
|
|
78
|
+
const scene = chart && provider.chartMaterialAt ? { kind: "materials", materialAt: provider.chartMaterialAt,
|
|
79
|
+
materials: [...viewChart.materials.map((m, index) => (m.kind === "neutral" ? { neutral: m.reflectance } : { radiance: Float64Array.from(wavelengths, (nm) => viewProvider.materialRadiance(index, nm)) })),
|
|
80
|
+
...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])],
|
|
81
|
+
whiteRadiance: Float64Array.from(wavelengths, (nm) => viewIrradianceScale * viewIlluminant.shape.evaluate(nm) / Math.PI) } :
|
|
82
|
+
{ kind: "radiance", radianceAt: viewProvider.radianceAt };
|
|
83
|
+
const channelIndex = (x, y) => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));
|
|
84
|
+
// Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear
|
|
85
|
+
// still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.
|
|
86
|
+
const projectionDistance = resolveProjectionDistanceMm(focalLengthMm, focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;
|
|
87
|
+
const nodePhases = apertureNodeOffsets(boxSamples).flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,
|
|
88
|
+
aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));
|
|
89
|
+
const queryPoint = (x, y, o, out) => {
|
|
90
|
+
const node = nodePhases[o];
|
|
91
|
+
const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;
|
|
92
|
+
const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;
|
|
93
|
+
const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);
|
|
94
|
+
const length = Math.hypot(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);
|
|
95
|
+
out[0] = rx / length * t / metresPerFrameUnit;
|
|
96
|
+
out[1] = ry / length * t / metresPerFrameUnit;
|
|
97
|
+
};
|
|
98
|
+
const signal = createDisplayViewExpectedSignal({ nativeWidth, nativeHeight, outputWidth: width, wavelengthsNm: wavelengths, electronsPerRadiance: perNode, offsets,
|
|
99
|
+
...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex });
|
|
100
|
+
// Readout moments per channel: normalized value k·e with variance k²·(e + σ_read²) plus the ADC quantization (1/12 code², times d² before a digital gain d).
|
|
101
|
+
const readout = CHANNELS.map((channel) => {
|
|
102
|
+
const { readoutProfile, regimeId } = regimeByChannel[channel];
|
|
103
|
+
const resolved = resolveSensorReadoutRegime({ profile: readoutProfile, regimeId });
|
|
104
|
+
const k = relativePerElectron[channel], digitalGain = resolved.regime.adc.digitalGain ?? 1, gain = resolved.regime.systemConversionGainElectronsPerCode.value;
|
|
105
|
+
const codeSpan = digitalGain / (gain * k), read = resolved.combinedReadNoiseRmsElectrons;
|
|
106
|
+
const quantizationCodes = digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12);
|
|
107
|
+
return { perElectron: k, darkElectrons: DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, constantVariance: k * k * read * read + quantizationCodes / (codeSpan * codeSpan) };
|
|
108
|
+
});
|
|
109
|
+
const phases = bilinearPhaseProfiles(), demosaicTable = [];
|
|
110
|
+
for (const phase of phases)
|
|
111
|
+
demosaicTable[phase.phaseY * 2 + phase.phaseX] = CHANNELS.map((c) => phase.profile.kernels.find((k) => k.outputChannelId === c).contributions
|
|
112
|
+
.map((t) => ({ dx: t.offsetX, dy: t.offsetY, weight: t.weight })));
|
|
113
|
+
const viewIntent = intentOf(viewWhiteBalanceState), viewGains = viewIntent ? [viewIntent.channelGains.red, viewIntent.channelGains.green, viewIntent.channelGains.blue] : null;
|
|
114
|
+
const colour = prepareSensorColorDevelopment(CAMERA_TO_XYZ, viewGains, highlightMode);
|
|
115
|
+
const toSrgb = resolveCaptureColorModel().xyzToCameraRgb, m = colour.cameraToXyz;
|
|
116
|
+
const matrix = [0, 1, 2].map((r) => [0, 1, 2].map((c) => toSrgb[r][0] * m[0][c] + toSrgb[r][1] * m[1][c] + toSrgb[r][2] * m[2][c]));
|
|
117
|
+
const viewRegion = v.region ?? { x: 0, y: 0, width: v.width, height: v.height };
|
|
118
|
+
// Reach of a footprint's pixels into the scene, in sites: the demosaic (1), the largest query offset and the kernel radius.
|
|
119
|
+
const reachSites = (ahd ? 3 : 1) + Math.ceil(Math.max(0, ...offsets.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y))))) + (kernels ? kernels.radius : 0);
|
|
120
|
+
const rendered = await renderDisplayView({ frameWidth: width, frameHeight: height, margin, nativeWidth, nativeHeight, viewWidth: v.width, viewHeight: v.height,
|
|
121
|
+
region: viewRegion, samplesPerAxis: v.samplesPerAxis, reachSites, bitDepth: v.bitDepth, seed: noiseSeed, signal, readout, demosaic: demosaicTable,
|
|
122
|
+
...(ahd ? { candidates: [0, 1, 2, 3].map((p) => ahdCandidateKernels(p % 2, Math.floor(p / 2))),
|
|
123
|
+
ahdCorrection: { labMatrix: CAMERA_TO_XYZ.map((row) => row.map((value, c) => value * (viewGains ? viewGains[c] : 1))), drawsPerState: DISPLAY_VIEW_AHD_CORRECTION_DRAWS,
|
|
124
|
+
thresholdVariance: DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD * DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD } } : {}),
|
|
125
|
+
gains: colour.appliedGains, clipLevel: Math.min(...colour.appliedGains), matrix, toneCurve: toneCurve,
|
|
126
|
+
yieldControl, checkAbort });
|
|
127
|
+
if (viewInternals) {
|
|
128
|
+
viewInternals.value = rendered;
|
|
129
|
+
viewInternals.signal = signal;
|
|
130
|
+
}
|
|
131
|
+
const viewMs = clockMs() - viewStarted, scaleX = width / v.width, scaleY = height / v.height;
|
|
132
|
+
assume("view.definition", "model", { resampling: "area-average-linear-light", width: v.width, height: v.height, frameWidth: width, frameHeight: height, scaleX, scaleY }, "The image the full-resolution capture of this request would give after resizing to " + v.width + " × " + v.height + ": each display pixel is the area-weighted mean, in linear light (sRGB samples decoded with the sRGB EOTF), " +
|
|
133
|
+
"of the real output pixels its rectangle covers (" + scaleX + " × " + scaleY + " real pixels), then sRGB-encoded and rounded. The real capture's own samples are rounded to the view's bit depth before averaging.");
|
|
134
|
+
const uniformSamplesPerAxis = Math.max(1, Math.round(v.samplesPerAxis / 4));
|
|
135
|
+
assume("view.estimation", "model", { method: "stratified-monte-carlo-over-real-pixels", samplesPerAxis: v.samplesPerAxis, uniformSamplesPerAxis,
|
|
136
|
+
detailPixels: rendered.detailPixelCount, uniformPixels: rendered.uniformPixelCount }, "Each display pixel's expected value is the mean over K × K strata of its rectangle, one seeded uniformly jittered real pixel in each, so it is unbiased; the error is zero where the rectangle is uniform " +
|
|
137
|
+
"and falls as K^-3/2 at an edge. K = " + v.samplesPerAxis + " where a fixed pilot grid over the rectangle (widened by the demosaic, filter and kernel reach) or a neighbouring pixel's grid finds more than one scene value, " +
|
|
138
|
+
"K = " + uniformSamplesPerAxis + " elsewhere. For each sampled pixel the expected electrons of the real sites its demosaic reads are exact at the real pitch (the scene sampled at those sites through the pixel aperture, " +
|
|
139
|
+
"anti-aliasing filter and diffraction kernels, with the capture's own query arithmetic, so the colour mosaic and its aliasing are the real sensor's), and the per-site clip at white, the highlight clip, the tone curve " +
|
|
140
|
+
"and the clip to [0, 1] act on the pixel's own noise distribution, approximated as normal.");
|
|
141
|
+
assume("view.noise", "model", { method: "variance-of-the-area-mean", distribution: "normal" }, "Noise of the mean of the real pixels: the covariance of the weighted sum of independent site noises (shot, dark, read and ADC), with the demosaic correlation (B_c = Σ β², in closed form from the rectangle) and the " +
|
|
142
|
+
"expected slopes of the clips and the tone curve (Stein's lemma); the variance the linearization misses and the real pixels' output rounding are added per pixel. One correlated normal draw per display pixel, seeded " +
|
|
143
|
+
"by sensor.noiseSeed: the same statistics as the resized capture, not the same realization.");
|
|
144
|
+
assume("view.demosaic", "model", ahd ? { capture: "ahd", view: "ahd-candidates-with-low-signal-correction", correctedPixels: rendered.ahdCorrectedPixelCount,
|
|
145
|
+
correctionDrawsPerState: DISPLAY_VIEW_AHD_CORRECTION_DRAWS, correctionThresholdLinearStd: DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD } : { capture: "bilinear", view: "bilinear" }, ahd ? "AHD is non-linear only in its choice between two linear candidates (green along the rows or the columns, red and blue from colour differences). The view propagates both candidates' exact linear statistics " +
|
|
146
|
+
"and averages them. Where a sampled pixel's noise exceeds " + DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD + " (linear light), the choice reacts to the noise and shifts the colour, so the pixel adds the mean " +
|
|
147
|
+
"difference between the engine's AHD steps and the two candidates on " + DISPLAY_VIEW_AHD_CORRECTION_DRAWS + " drawn noisy flat neighbourhoods (Poisson electrons plus read and ADC noise) with its channel means, " +
|
|
148
|
+
"computed once per state of a logarithmic grid (8 steps per octave of the channel means) and interpolated trilinearly, a control variate with no per-pixel sampling noise; " +
|
|
149
|
+
"the flat-neighbourhood state is an approximation at edges (docs/DISPLAY_VIEW.md, validation)." :
|
|
150
|
+
"The bilinear demosaic's linear kernels, exactly.");
|
|
151
|
+
assume("view.source", "model", { rendering: "srgb", format: v.format, ignoredCaptureOutput: ["format", "colorEncoding", "bitDepth", "gamutMapping", "jpegQuantizationStep", "dng", "metadata", "maximumPixels"].filter((k) => outputOptions[k] !== undefined) }, "The view shows the capture's sRGB rendering (its tone curve, white balance and highlight mode); the capture's file-format options do not apply to it.");
|
|
152
|
+
return Object.freeze({ kind: "display-view", label: "Display view: the full-resolution capture resized to " + v.width + " × " + v.height + " (estimated); not a capture.",
|
|
153
|
+
facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,
|
|
154
|
+
image: { width: viewRegion.width, height: viewRegion.height, format: v.format, samples: rendered.samples },
|
|
155
|
+
view: { width: v.width, height: v.height, frameWidth: width, frameHeight: height, scaleX, scaleY, samplesPerAxis: v.samplesPerAxis, ...(v.region ? { region: { ...v.region } } : {}) },
|
|
156
|
+
exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds) },
|
|
157
|
+
scene: { sceneId: chart ? "chart:" + chart.preset : callerScene.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},
|
|
158
|
+
illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,
|
|
159
|
+
framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },
|
|
160
|
+
diagnostics: { sampleCount: rendered.sampleCount, siteEvaluations: rendered.siteCount, sceneLookups: signal.lookups, detailPixelCount: rendered.detailPixelCount,
|
|
161
|
+
uniformPixelCount: rendered.uniformPixelCount, ahdCorrectedPixelCount: rendered.ahdCorrectedPixelCount, timingsMs: { view: viewMs, total: Date.now() - started } },
|
|
162
|
+
provenance: { scientificStatus: "approximation", cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion, evidence: [...FACADE_EVIDENCE, ...preset.evidence],
|
|
163
|
+
composedContracts: ["calculateEnvironmentSensorPhotoSignal", "resolveSensorReadoutRegime", "calculateSiteEffectiveSaturationCode", ...(diffraction ? ["createDiffractionLimitedPsfProfile", "resolveLensPsfConvolutionKernels"] : []),
|
|
164
|
+
...(antiAliasing ? ["resolveAntiAliasingSpatialKernel"] : []), "prepareSensorColorDevelopment", "display view estimator"],
|
|
165
|
+
limitations: ["Estimated display view of the full-resolution capture: its expected value is a stratified Monte Carlo estimate and its noise is drawn from the variance of the area mean, so it has the resized capture's statistics, not its exact pixels; not a capture and not a measurement."] },
|
|
166
|
+
assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });
|
|
167
|
+
}
|
|
168
|
+
//# sourceMappingURL=simulate-display-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"simulate-display-view.js","sourceRoot":"","sources":["../../src/facade/simulate-display-view.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * The public entry point of the display view (`simulateDisplayView`) and its renderer. `simulate-capture.ts` resolves the\n * request (`runCapture`) and calls the renderer it is given, so a bundle that imports only `simulateCapture` or\n * `simulateIdealReference` does not contain the display view (`display-view.ts`) or the portable evaluations it uses.\n * The design and its error analysis are in docs/DISPLAY_VIEW.md.\n */\n\nimport { ENGINE_API_VERSION } from \"../core/version.js\";\nimport { resolvePortableColorTemperatureWhiteBalance, type ResolvedWhiteBalanceState } from \"../color/white-balance.js\";\nimport { resolveCaptureColorModel } from \"../color/capture-color.js\";\nimport { prepareSensorColorDevelopment } from \"../color/sensor-color-development.js\";\nimport { resolveLensPsfConvolutionKernels } from \"../optics/lens-psf-kernels.js\";\nimport { resolveProjectionDistanceMm } from \"../motion/camera-rotation.js\";\nimport { resolveSensorReadoutRegime } from \"../sensor/raw-readout.js\";\nimport { resolvePortableChartScene, type ChartScene } from \"./chart-scenes.js\";\nimport { resolvePortableCaptureIlluminant, calculateIlluminantIrradianceScale } from \"./scene-radiance.js\";\nimport { ahdCandidateKernels, createDisplayViewExpectedSignal, renderDisplayView, type DisplayViewChannelReadout, type DisplayViewExpectedSignal, type DisplayViewRenderResult,\n type DisplayViewScene } from \"./display-view.js\";\nimport { runCapture, apertureNodeOffsets, bilinearPhaseProfiles, clockMs, fail, sceneIlluminantWhiteBalance, CAMERA_TO_XYZ, CHANNELS, DARK_CURRENT_ELECTRONS_PER_SECOND, FACADE_EVIDENCE,\n SIMULATE_CAPTURE_FACADE_VERSION, type DisplayViewResult, type DisplayViewRunContext, type SimulateCaptureOptions, type SimulateDisplayViewOptions } from \"./simulate-capture.js\";\n\n/**\n * The display view of a capture request: the image the real full-resolution capture (`simulateCapture(options)`) would\n * give after it is resized to `view.width` × `view.height` by area averaging in linear light, estimated at a cost that\n * follows the display pixels, not the capture's. Optics (lens diffraction, the anti-aliasing filter, the pixel aperture)\n * act at the real pixel pitch; the colour mosaic, demosaic and development are those of the real pixels; the noise is\n * that of the average of the real pixels, demosaic correlation included; exposure, white balance and tone are the\n * capture's. The result is labelled `kind: \"display-view\"`; it is not a capture.\n * @see docs/DISPLAY_VIEW.md and docs/QUICK_START.md, \"Display view\".\n */\nexport async function simulateDisplayView(options: SimulateCaptureOptions, view: SimulateDisplayViewOptions): Promise<DisplayViewResult> {\n return await runCapture(options, \"display-view\", view, (context) => renderDisplayViewResult(context)) as DisplayViewResult;\n}\n/**\n * Internal (not exported from the package): the display view plus its noise-free expected linear values and noise standard\n * deviations, for the validation scripts and tests.\n */\nexport async function simulateDisplayViewWithInternals(options: SimulateCaptureOptions, view: SimulateDisplayViewOptions):\n Promise<{ result: DisplayViewResult; internals: DisplayViewRenderResult; signal: DisplayViewExpectedSignal }> {\n const internals: { value?: DisplayViewRenderResult; signal?: DisplayViewExpectedSignal } = {};\n const result = await runCapture(options, \"display-view\", view, (context) => renderDisplayViewResult(context, internals)) as DisplayViewResult;\n return { result, internals: internals.value!, signal: internals.signal! };\n}\n\n/** AHD low-signal correction (display view): draws per flat-neighbourhood state, and the pixel noise (linear light) above which it applies. */\nconst DISPLAY_VIEW_AHD_CORRECTION_DRAWS = 2048, DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD = 0.01;\n\n/** Renders the display view of a resolved request (the last step of `runCapture` for \"display-view\"). */\nasync function renderDisplayViewResult(context: DisplayViewRunContext, viewInternals?: { value?: DisplayViewRenderResult; signal?: DisplayViewExpectedSignal }): Promise<DisplayViewResult> {\n const { ahd, antiAliasing, aspect, assume, assumptions, axisChannelResponse, boxSamples, buildProvider, callerScene, chart, chartReport, chartSize, diffraction, diffractionPsf, fNumber, focalLengthMm, focusDistanceM, height, highlightMode, illuminanceLux, illuminant, illuminantOption, imagingArea, intentOf, metresPerFrameUnit, noiseSeed, outputOptions, parsedView, pitch, preset, provider, regimeByChannel, resolvedIso, sceneOption, scenePreset, shutterSeconds, started, subjectDistanceM, toneCurve, whiteBalanceOption, whiteBalanceState, width, checkAbort, margin, mode, nativeHeight, nativeWidth, sourceSha256, yieldControl } = context;\n const kelvinInput = context.kelvinWhiteBalanceInput;\n const portableKelvinWhiteBalance = kelvinInput ? (): ResolvedWhiteBalanceState => resolvePortableColorTemperatureWhiteBalance(kelvinInput) : undefined;\n const viewStarted = clockMs(), v = parsedView!;\n // The engine's illuminant formulas (Planckian, CIE A, daylight locus) evaluated with the portable power and e^x − 1, so\n // the view's radiances, irradiance scale and scene-illuminant white balance have the same bits in every JavaScript\n // engine; they differ from the capture's own by at most a few ulps (docs/DISPLAY_VIEW.md, \"Same bytes in every engine\").\n const viewIlluminant = resolvePortableCaptureIlluminant(illuminantOption), viewIrradianceScale = calculateIlluminantIrradianceScale(viewIlluminant.shape, illuminanceLux);\n const viewProvider = buildProvider(viewIlluminant, viewIrradianceScale);\n const viewWhiteBalanceState = whiteBalanceOption === \"scene-illuminant\" ? sceneIlluminantWhiteBalance(viewIlluminant) : portableKelvinWhiteBalance ? portableKelvinWhiteBalance() : whiteBalanceState;\n // The chart's materials likewise (a step wedge's geometric levels use the portable power); same geometry and order.\n const viewChart = chart ? resolvePortableChartScene({ scene: sceneOption as ChartScene, aspect: chartSize ? chartSize.heightM / chartSize.widthM : aspect }) : undefined;\n if (chart && viewChart!.materials.length !== chart.materials.length) fail(\"simulateDisplayView: the chart's materials could not be resolved portably.\");\n const { electronsPerRadiance, wavelengths, relativePerElectron } = axisChannelResponse(\"simulateDisplayView\");\n const perNode = CHANNELS.map((c) => Float64Array.from(wavelengths, (nm) => electronsPerRadiance[c].get(nm) ?? 0)) as [Float64Array, Float64Array, Float64Array];\n // Query offsets of each site: pixel-aperture nodes minus anti-aliasing split offsets (sites, +x right, +y down), as the environment forms them.\n const spots = antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 }, normalizedWeight: 1 }];\n const offsets = apertureNodeOffsets(boxSamples).flatMap((node) => spots.map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch,\n weight: c.normalizedWeight / (boxSamples * boxSamples) })));\n // Diffraction: the same per-node site kernels as the object-space convolution (createDiffractedPlanarProvider).\n let kernels: { radius: number; perNode: Float64Array[] } | undefined;\n if (diffraction) {\n const radius = diffraction.kernelRadiusSamples;\n kernels = { radius, perNode: wavelengths.map((wavelengthNm) => {\n if (!diffraction!.profile.axes.wavelengthNm.includes(wavelengthNm)) fail(\"simulateDisplayView: a spectral node has no diffraction kernel.\");\n const k = resolveLensPsfConvolutionKernels({ schemaVersion: \"0.1.0\", psf: diffractionPsf!.configuration.psf, spectralWeights: [{ wavelengthNm, weight: 1 }],\n grid: { width: 1, height: 1, sitePitchMicrometers: { x: pitch, y: pitch }, opticalAxisSite: { x: 0.5, y: 0.5 } }, patchWidth: 1, patchHeight: 1, kernelRadius: radius }).value;\n if (!k.exactSiteSampling || k.kernelRadius !== radius) fail(\"simulateDisplayView: diffraction kernels must sample the site pitch exactly.\");\n return Float64Array.from(k.kernels);\n }) };\n }\n const scene: DisplayViewScene = chart && provider.chartMaterialAt ? { kind: \"materials\", materialAt: provider.chartMaterialAt,\n materials: [...viewChart!.materials.map((m, index) => (m.kind === \"neutral\" ? { neutral: m.reflectance } : { radiance: Float64Array.from(wavelengths, (nm) => viewProvider.materialRadiance!(index, nm)) })),\n ...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])],\n whiteRadiance: Float64Array.from(wavelengths, (nm) => viewIrradianceScale * viewIlluminant.shape.evaluate(nm) / Math.PI) } :\n { kind: \"radiance\", radianceAt: viewProvider.radianceAt };\n const channelIndex = (x: number, y: number): number => ((y & 1) === 0 ? ((x & 1) === 0 ? 0 : 1) : ((x & 1) === 0 ? 1 : 2));\n // Without diffraction the environment's own query arithmetic (native-environment-lean.ts `geometry`, then the rectilinear\n // still-camera ray and the planar provider's plane point), so boundary points resolve as in the capture.\n const projectionDistance = resolveProjectionDistanceMm(focalLengthMm, focusDistanceM).projectionDistanceMm, siteWidthMm = pitch / 1000;\n const nodePhases = apertureNodeOffsets(boxSamples).flatMap((_, index) => spots.map((c) => ({ xPhase: (index % boxSamples + 0.5) / boxSamples, yPhase: (Math.floor(index / boxSamples) + 0.5) / boxSamples,\n aaX: c.offsetMicrometers.x / 1000, aaY: c.offsetMicrometers.y / 1000 })));\n const queryPoint = (x: number, y: number, o: number, out: Float64Array): void => {\n const node = nodePhases[o]!;\n const centerX = -imagingArea.widthMm / 2 + (pitch / 2 + x * pitch + 0) / 1000, centerY = -imagingArea.heightMm / 2 + (pitch / 2 + y * pitch + 0) / 1000;\n const sourceX = (centerX - siteWidthMm / 2 + node.xPhase * siteWidthMm) - node.aaX, sourceY = (centerY - siteWidthMm / 2 + node.yPhase * siteWidthMm) - node.aaY;\n const rx = projectionDistance * (sourceX / projectionDistance), ry = projectionDistance * (-sourceY / projectionDistance);\n const length = Math.hypot(rx, ry, projectionDistance), t = subjectDistanceM / (projectionDistance / length);\n out[0] = rx / length * t / metresPerFrameUnit; out[1] = ry / length * t / metresPerFrameUnit;\n };\n const signal = createDisplayViewExpectedSignal({ nativeWidth, nativeHeight, outputWidth: width, wavelengthsNm: wavelengths, electronsPerRadiance: perNode, offsets,\n ...(kernels ? { kernels } : { queryPoint }), scene, channelAt: channelIndex });\n // Readout moments per channel: normalized value k·e with variance k²·(e + σ_read²) plus the ADC quantization (1/12 code², times d² before a digital gain d).\n const readout = CHANNELS.map((channel): DisplayViewChannelReadout => {\n const { readoutProfile, regimeId } = regimeByChannel[channel];\n const resolved = resolveSensorReadoutRegime({ profile: readoutProfile as Parameters<typeof resolveSensorReadoutRegime>[0][\"profile\"], regimeId });\n const k = relativePerElectron[channel], digitalGain = resolved.regime.adc.digitalGain ?? 1, gain = resolved.regime.systemConversionGainElectronsPerCode.value;\n const codeSpan = digitalGain / (gain * k), read = resolved.combinedReadNoiseRmsElectrons;\n const quantizationCodes = digitalGain * digitalGain / 12 + (digitalGain === 1 ? 0 : 1 / 12);\n return { perElectron: k, darkElectrons: DARK_CURRENT_ELECTRONS_PER_SECOND * shutterSeconds, constantVariance: k * k * read * read + quantizationCodes / (codeSpan * codeSpan) };\n }) as [DisplayViewChannelReadout, DisplayViewChannelReadout, DisplayViewChannelReadout];\n const phases = bilinearPhaseProfiles(), demosaicTable: { dx: number; dy: number; weight: number }[][][] = [];\n for (const phase of phases) demosaicTable[phase.phaseY * 2 + phase.phaseX] = CHANNELS.map((c) => phase.profile.kernels.find((k) => k.outputChannelId === c)!.contributions\n .map((t) => ({ dx: t.offsetX, dy: t.offsetY, weight: t.weight })));\n const viewIntent = intentOf(viewWhiteBalanceState), viewGains = viewIntent ? [viewIntent.channelGains.red, viewIntent.channelGains.green, viewIntent.channelGains.blue] : null;\n const colour = prepareSensorColorDevelopment(CAMERA_TO_XYZ as unknown as Parameters<typeof prepareSensorColorDevelopment>[0], viewGains, highlightMode);\n const toSrgb = resolveCaptureColorModel().xyzToCameraRgb, m = colour.cameraToXyz;\n const matrix = [0, 1, 2].map((r) => [0, 1, 2].map((c) => toSrgb[r]![0]! * m[0]![c]! + toSrgb[r]![1]! * m[1]![c]! + toSrgb[r]![2]! * m[2]![c]!));\n const viewRegion = v.region ?? { x: 0, y: 0, width: v.width, height: v.height };\n // Reach of a footprint's pixels into the scene, in sites: the demosaic (1), the largest query offset and the kernel radius.\n const reachSites = (ahd ? 3 : 1) + Math.ceil(Math.max(0, ...offsets.map((o) => Math.max(Math.abs(o.x), Math.abs(o.y))))) + (kernels ? kernels.radius : 0);\n const rendered = await renderDisplayView({ frameWidth: width, frameHeight: height, margin, nativeWidth, nativeHeight, viewWidth: v.width, viewHeight: v.height,\n region: viewRegion, samplesPerAxis: v.samplesPerAxis, reachSites, bitDepth: v.bitDepth, seed: noiseSeed, signal, readout, demosaic: demosaicTable,\n ...(ahd ? { candidates: [0, 1, 2, 3].map((p) => ahdCandidateKernels(p % 2, Math.floor(p / 2))),\n ahdCorrection: { labMatrix: CAMERA_TO_XYZ.map((row) => row.map((value, c) => value * (viewGains ? viewGains[c]! : 1))), drawsPerState: DISPLAY_VIEW_AHD_CORRECTION_DRAWS,\n thresholdVariance: DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD * DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD } } : {}),\n gains: colour.appliedGains as [number, number, number], clipLevel: Math.min(...colour.appliedGains), matrix, toneCurve: toneCurve as \"identity\" | \"positive-reinhard-per-channel\",\n yieldControl, checkAbort });\n if (viewInternals) { viewInternals.value = rendered; viewInternals.signal = signal; }\n const viewMs = clockMs() - viewStarted, scaleX = width / v.width, scaleY = height / v.height;\n assume(\"view.definition\", \"model\", { resampling: \"area-average-linear-light\", width: v.width, height: v.height, frameWidth: width, frameHeight: height, scaleX, scaleY },\n \"The image the full-resolution capture of this request would give after resizing to \" + v.width + \" × \" + v.height + \": each display pixel is the area-weighted mean, in linear light (sRGB samples decoded with the sRGB EOTF), \" +\n \"of the real output pixels its rectangle covers (\" + scaleX + \" × \" + scaleY + \" real pixels), then sRGB-encoded and rounded. The real capture's own samples are rounded to the view's bit depth before averaging.\");\n const uniformSamplesPerAxis = Math.max(1, Math.round(v.samplesPerAxis / 4));\n assume(\"view.estimation\", \"model\", { method: \"stratified-monte-carlo-over-real-pixels\", samplesPerAxis: v.samplesPerAxis, uniformSamplesPerAxis,\n detailPixels: rendered.detailPixelCount, uniformPixels: rendered.uniformPixelCount },\n \"Each display pixel's expected value is the mean over K × K strata of its rectangle, one seeded uniformly jittered real pixel in each, so it is unbiased; the error is zero where the rectangle is uniform \" +\n \"and falls as K^-3/2 at an edge. K = \" + v.samplesPerAxis + \" where a fixed pilot grid over the rectangle (widened by the demosaic, filter and kernel reach) or a neighbouring pixel's grid finds more than one scene value, \" +\n \"K = \" + uniformSamplesPerAxis + \" elsewhere. For each sampled pixel the expected electrons of the real sites its demosaic reads are exact at the real pitch (the scene sampled at those sites through the pixel aperture, \" +\n \"anti-aliasing filter and diffraction kernels, with the capture's own query arithmetic, so the colour mosaic and its aliasing are the real sensor's), and the per-site clip at white, the highlight clip, the tone curve \" +\n \"and the clip to [0, 1] act on the pixel's own noise distribution, approximated as normal.\");\n assume(\"view.noise\", \"model\", { method: \"variance-of-the-area-mean\", distribution: \"normal\" },\n \"Noise of the mean of the real pixels: the covariance of the weighted sum of independent site noises (shot, dark, read and ADC), with the demosaic correlation (B_c = Σ β², in closed form from the rectangle) and the \" +\n \"expected slopes of the clips and the tone curve (Stein's lemma); the variance the linearization misses and the real pixels' output rounding are added per pixel. One correlated normal draw per display pixel, seeded \" +\n \"by sensor.noiseSeed: the same statistics as the resized capture, not the same realization.\");\n assume(\"view.demosaic\", \"model\", ahd ? { capture: \"ahd\", view: \"ahd-candidates-with-low-signal-correction\", correctedPixels: rendered.ahdCorrectedPixelCount,\n correctionDrawsPerState: DISPLAY_VIEW_AHD_CORRECTION_DRAWS, correctionThresholdLinearStd: DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD } : { capture: \"bilinear\", view: \"bilinear\" },\n ahd ? \"AHD is non-linear only in its choice between two linear candidates (green along the rows or the columns, red and blue from colour differences). The view propagates both candidates' exact linear statistics \" +\n \"and averages them. Where a sampled pixel's noise exceeds \" + DISPLAY_VIEW_AHD_CORRECTION_THRESHOLD + \" (linear light), the choice reacts to the noise and shifts the colour, so the pixel adds the mean \" +\n \"difference between the engine's AHD steps and the two candidates on \" + DISPLAY_VIEW_AHD_CORRECTION_DRAWS + \" drawn noisy flat neighbourhoods (Poisson electrons plus read and ADC noise) with its channel means, \" +\n \"computed once per state of a logarithmic grid (8 steps per octave of the channel means) and interpolated trilinearly, a control variate with no per-pixel sampling noise; \" +\n \"the flat-neighbourhood state is an approximation at edges (docs/DISPLAY_VIEW.md, validation).\" :\n \"The bilinear demosaic's linear kernels, exactly.\");\n assume(\"view.source\", \"model\", { rendering: \"srgb\", format: v.format, ignoredCaptureOutput: [\"format\", \"colorEncoding\", \"bitDepth\", \"gamutMapping\", \"jpegQuantizationStep\", \"dng\", \"metadata\", \"maximumPixels\"].filter((k) => outputOptions[k] !== undefined) },\n \"The view shows the capture's sRGB rendering (its tone curve, white balance and highlight mode); the capture's file-format options do not apply to it.\");\n return Object.freeze({ kind: \"display-view\" as const, label: \"Display view: the full-resolution capture resized to \" + v.width + \" × \" + v.height + \" (estimated); not a capture.\",\n facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, engineApiVersion: ENGINE_API_VERSION,\n image: { width: viewRegion.width, height: viewRegion.height, format: v.format, samples: rendered.samples },\n view: { width: v.width, height: v.height, frameWidth: width, frameHeight: height, scaleX, scaleY, samplesPerAxis: v.samplesPerAxis, ...(v.region ? { region: { ...v.region } } : {}) },\n exposure: { mode, settings: { fNumber, shutterSeconds, iso: resolvedIso }, ev100: Math.log2(fNumber ** 2 / shutterSeconds) },\n scene: { sceneId: chart ? \"chart:\" + chart.preset : callerScene!.sceneId, preset: scenePreset, parameters: chart ? chart.parameters : {},\n illuminant: { id: illuminant.id, description: illuminant.description, illuminanceLux }, distanceM: subjectDistanceM,\n framedWidthM: 2 * metresPerFrameUnit, framedHeightM: 2 * metresPerFrameUnit * aspect, ...(chartReport ? { chart: chartReport } : {}), sourceSha256 },\n diagnostics: { sampleCount: rendered.sampleCount, siteEvaluations: rendered.siteCount, sceneLookups: signal.lookups, detailPixelCount: rendered.detailPixelCount,\n uniformPixelCount: rendered.uniformPixelCount, ahdCorrectedPixelCount: rendered.ahdCorrectedPixelCount, timingsMs: { view: viewMs, total: Date.now() - started } },\n provenance: { scientificStatus: \"approximation\" as const, cameraPresetId: preset.presetId, cameraPresetVersion: preset.presetVersion, evidence: [...FACADE_EVIDENCE, ...preset.evidence],\n composedContracts: [\"calculateEnvironmentSensorPhotoSignal\", \"resolveSensorReadoutRegime\", \"calculateSiteEffectiveSaturationCode\", ...(diffraction ? [\"createDiffractionLimitedPsfProfile\", \"resolveLensPsfConvolutionKernels\"] : []),\n ...(antiAliasing ? [\"resolveAntiAliasingSpatialKernel\"] : []), \"prepareSensorColorDevelopment\", \"display view estimator\"],\n limitations: [\"Estimated display view of the full-resolution capture: its expected value is a stratified Monte Carlo estimate and its noise is drawn from the variance of the area mean, so it has the resized capture's statistics, not its exact pixels; not a capture and not a measurement.\"] },\n assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });\n}\n"]}
|
|
@@ -42,6 +42,14 @@ export interface SubjectFraming {
|
|
|
42
42
|
frameWidthFraction: number;
|
|
43
43
|
frameHeightFraction: number;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance
|
|
47
|
+
* relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor
|
|
48
|
+
* `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).
|
|
49
|
+
*/
|
|
50
|
+
closeUpTransmission: number;
|
|
51
|
+
/** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */
|
|
52
|
+
closeUpLossStops: number;
|
|
45
53
|
/** The generic prime of this equivalent focal length, when there is one. */
|
|
46
54
|
prime: GenericPrimeLens | null;
|
|
47
55
|
/** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */
|
|
@@ -53,3 +61,73 @@ export interface SubjectFraming {
|
|
|
53
61
|
}
|
|
54
62
|
/** The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3). */
|
|
55
63
|
export declare function calculateSubjectDistanceForFraming(input: CalculateSubjectDistanceForFramingInput): SubjectFraming;
|
|
64
|
+
export interface CalculateChartFramingInput {
|
|
65
|
+
/** Active sensor (or simulated frame) size, mm: for `simulateCapture`, output width × height × pixel pitch. */
|
|
66
|
+
sensor: {
|
|
67
|
+
widthMm: number;
|
|
68
|
+
heightMm: number;
|
|
69
|
+
};
|
|
70
|
+
/** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */
|
|
71
|
+
focalLengthMm?: number;
|
|
72
|
+
/** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens (as `lens.prime`). */
|
|
73
|
+
equivalentFocalLengthMm?: number;
|
|
74
|
+
/** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */
|
|
75
|
+
chart: {
|
|
76
|
+
widthM: number;
|
|
77
|
+
heightM?: number;
|
|
78
|
+
};
|
|
79
|
+
/** Distance from the lens to the chart plane, metres; more than the focal length. */
|
|
80
|
+
subjectDistanceM: number;
|
|
81
|
+
/** Focus distance, metres (as `lens.focusDistanceM`); default the subject distance. More than the focal length. */
|
|
82
|
+
focusDistanceM?: number;
|
|
83
|
+
}
|
|
84
|
+
export interface ChartFraming {
|
|
85
|
+
subjectDistanceM: number;
|
|
86
|
+
focusDistanceM: number;
|
|
87
|
+
/** Thin-lens image distance at the focus distance, f·s/(s − f), mm. */
|
|
88
|
+
imageDistanceMm: number;
|
|
89
|
+
/** Image size ÷ object size on the chart plane, v/d: f/(d − f) when focused on the chart. */
|
|
90
|
+
magnification: number;
|
|
91
|
+
focalLengthMm: number;
|
|
92
|
+
equivalentFocalLengthMm: number;
|
|
93
|
+
cropFactor35Mm: number;
|
|
94
|
+
chart: {
|
|
95
|
+
widthM: number;
|
|
96
|
+
heightM: number;
|
|
97
|
+
};
|
|
98
|
+
/** Plane area the frame covers at the subject distance, metres: sensor side ÷ magnification. */
|
|
99
|
+
framedWidthM: number;
|
|
100
|
+
framedHeightM: number;
|
|
101
|
+
/** Fraction of the frame width and height the chart covers; above 1 means the chart is larger than the frame and cropped. */
|
|
102
|
+
coverage: {
|
|
103
|
+
frameWidthFraction: number;
|
|
104
|
+
frameHeightFraction: number;
|
|
105
|
+
};
|
|
106
|
+
/** The chart's image on the sensor, mm. */
|
|
107
|
+
chartImageMm: {
|
|
108
|
+
width: number;
|
|
109
|
+
height: number;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance
|
|
113
|
+
* relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor
|
|
114
|
+
* `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).
|
|
115
|
+
*/
|
|
116
|
+
closeUpTransmission: number;
|
|
117
|
+
/** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */
|
|
118
|
+
closeUpLossStops: number;
|
|
119
|
+
/** The generic prime of this equivalent focal length, when there is one. */
|
|
120
|
+
prime: GenericPrimeLens | null;
|
|
121
|
+
/** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */
|
|
122
|
+
minimumFocusDistanceM: number;
|
|
123
|
+
/** True when the subject or focus distance is closer than the minimum focus distance (`simulateCapture` with `lens.prime` rejects it). */
|
|
124
|
+
belowMinimumFocus: boolean;
|
|
125
|
+
scientificStatus: "approximation";
|
|
126
|
+
assumptions: readonly SimulateCaptureAssumption[];
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* How much of the frame a chart of a given physical size covers at any subject distance (2.5): magnification, framed plane size,
|
|
130
|
+
* coverage fractions (above 1 when cropped), the chart's image size and the minimum-focus check. The same thin-lens framing as
|
|
131
|
+
* `simulateCapture` (`scene.framing`, `result.scene.chart`) and `calculateSubjectDistanceForFraming`.
|
|
132
|
+
*/
|
|
133
|
+
export declare function calculateChartFraming(input: CalculateChartFramingInput): ChartFraming;
|
|
@@ -4,8 +4,12 @@
|
|
|
4
4
|
* images at v = f·d/(d − f) with magnification m = v/d = f/(d − f), so the distance for a magnification m is
|
|
5
5
|
* d = f·(1 + 1/m). This is the same framing the capture facade uses (`scene.framing`, `chartSize`), so a chart
|
|
6
6
|
* placed at the returned distance with `chartSize` fills the frame as reported.
|
|
7
|
-
*
|
|
7
|
+
*
|
|
8
|
+
* 2.5: `calculateChartFraming` is the inverse question: at a given subject distance (and focus distance), how much of the
|
|
9
|
+
* frame does the chart cover? It uses the facade's own framing, m = v/d with v = f·s/(s − f) at the focus distance s.
|
|
10
|
+
* @see docs/QUICK_START.md, "Fitting a chart to the frame" and "Chart coverage at any distance (2.5)".
|
|
8
11
|
*/
|
|
12
|
+
import { thinLensWorkingFNumberFactor } from "../optics/scene-to-sensor-irradiance.js";
|
|
9
13
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
10
14
|
import { requireInputObject } from "../core/record-validation.js";
|
|
11
15
|
import { calculateEquivalentFocalLength35Mm } from "../camera/equivalent-focal-length.js";
|
|
@@ -15,33 +19,40 @@ function positive(value, name) {
|
|
|
15
19
|
throw new InvalidConfigurationError(name + " must be a positive finite number.");
|
|
16
20
|
return value;
|
|
17
21
|
}
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
requireInputObject(input, "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
requireInputObject(
|
|
26
|
-
|
|
22
|
+
/** Shared input checks and lens resolution of the two framing functions. */
|
|
23
|
+
function resolveFramingInput(input, name, extraKeys) {
|
|
24
|
+
requireInputObject(input, name + " input");
|
|
25
|
+
const record = input;
|
|
26
|
+
for (const key of Object.keys(record))
|
|
27
|
+
if (!["sensor", "focalLengthMm", "equivalentFocalLengthMm", "chart", ...extraKeys].includes(key))
|
|
28
|
+
throw new InvalidConfigurationError(name + " input has unknown field \"" + key + "\".");
|
|
29
|
+
requireInputObject(record.sensor, "sensor");
|
|
30
|
+
requireInputObject(record.chart, "chart");
|
|
31
|
+
const sensor = record.sensor, chart = record.chart;
|
|
32
|
+
for (const key of Object.keys(sensor))
|
|
27
33
|
if (key !== "widthMm" && key !== "heightMm")
|
|
28
34
|
throw new InvalidConfigurationError("Unknown sensor field: " + key + ".");
|
|
29
|
-
for (const key of Object.keys(
|
|
35
|
+
for (const key of Object.keys(chart))
|
|
30
36
|
if (key !== "widthM" && key !== "heightM")
|
|
31
37
|
throw new InvalidConfigurationError("Unknown chart field: " + key + ".");
|
|
32
|
-
const sensorW = positive(
|
|
33
|
-
if ((
|
|
38
|
+
const sensorW = positive(sensor.widthMm, "sensor.widthMm"), sensorH = positive(sensor.heightMm, "sensor.heightMm");
|
|
39
|
+
if ((record.focalLengthMm === undefined) === (record.equivalentFocalLengthMm === undefined))
|
|
34
40
|
throw new InvalidConfigurationError("Give exactly one of focalLengthMm and equivalentFocalLengthMm.");
|
|
35
41
|
const crop = calculateEquivalentFocalLength35Mm({ focalLengthMm: 1, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }).value.cropFactor35Mm;
|
|
36
42
|
const assumptions = [];
|
|
37
|
-
const equivalent =
|
|
38
|
-
const focalLengthMm =
|
|
39
|
-
const prime = GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM.includes(equivalent) &&
|
|
43
|
+
const equivalent = record.equivalentFocalLengthMm === undefined ? positive(record.focalLengthMm, "focalLengthMm") * crop : positive(record.equivalentFocalLengthMm, "equivalentFocalLengthMm");
|
|
44
|
+
const focalLengthMm = record.focalLengthMm === undefined ? equivalent / crop : record.focalLengthMm;
|
|
45
|
+
const prime = GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM.includes(equivalent) && record.equivalentFocalLengthMm !== undefined ?
|
|
40
46
|
resolveGenericPrimeLens({ equivalentFocalLengthMm: equivalent, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }) : null;
|
|
41
|
-
const widthM = positive(
|
|
42
|
-
const heightM =
|
|
43
|
-
if (
|
|
47
|
+
const widthM = positive(chart.widthM, "chart.widthM");
|
|
48
|
+
const heightM = chart.heightM === undefined ? widthM * sensorH / sensorW : positive(chart.heightM, "chart.heightM");
|
|
49
|
+
if (chart.heightM === undefined)
|
|
44
50
|
assumptions.push({ id: "chart.heightM", kind: "default", value: heightM, explanation: "The chart has the sensor's shape: height = width × sensor height / sensor width." });
|
|
51
|
+
return { record, sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions };
|
|
52
|
+
}
|
|
53
|
+
/** The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3). */
|
|
54
|
+
export function calculateSubjectDistanceForFraming(input) {
|
|
55
|
+
const { sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions } = resolveFramingInput(input, "calculateSubjectDistanceForFraming", ["fit"]);
|
|
45
56
|
const fit = input.fit ?? "contain";
|
|
46
57
|
if (!["contain", "width", "height", "cover"].includes(fit))
|
|
47
58
|
throw new InvalidConfigurationError("fit must be contain, width, height or cover.");
|
|
@@ -60,7 +71,45 @@ export function calculateSubjectDistanceForFraming(input) {
|
|
|
60
71
|
explanation: "Equivalent focal length ÷ diagonal crop factor " + crop + " (calculateEquivalentFocalLength35Mm, 36 × 24 mm reference)." });
|
|
61
72
|
return Object.freeze({ subjectDistanceM, magnification: m, imageDistanceMm: focalLengthMm * (1 + m), focalLengthMm, equivalentFocalLengthMm: equivalent, cropFactor35Mm: crop,
|
|
62
73
|
chart: { widthM, heightM }, fit, framedWidthM: sensorW / 1000 / m, framedHeightM: sensorH / 1000 / m,
|
|
63
|
-
coverage: { frameWidthFraction: widthM * 1000 * m / sensorW, frameHeightFraction: heightM * 1000 * m / sensorH }, prime, minimumFocusDistanceM,
|
|
74
|
+
coverage: { frameWidthFraction: widthM * 1000 * m / sensorW, frameHeightFraction: heightM * 1000 * m / sensorH }, ...closeUpLight(focalLengthMm, subjectDistanceM), prime, minimumFocusDistanceM,
|
|
64
75
|
belowMinimumFocus: subjectDistanceM < minimumFocusDistanceM, scientificStatus: "approximation", assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });
|
|
65
76
|
}
|
|
77
|
+
/** 2.6: the close-up light factor at a focus distance, from the irradiance bridge's own working f-number factor. */
|
|
78
|
+
function closeUpLight(focalLengthMm, focusDistanceM) {
|
|
79
|
+
const factor = thinLensWorkingFNumberFactor(focalLengthMm, focusDistanceM);
|
|
80
|
+
return { closeUpTransmission: 1 / (factor * factor), closeUpLossStops: 2 * Math.log2(factor) };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* How much of the frame a chart of a given physical size covers at any subject distance (2.5): magnification, framed plane size,
|
|
84
|
+
* coverage fractions (above 1 when cropped), the chart's image size and the minimum-focus check. The same thin-lens framing as
|
|
85
|
+
* `simulateCapture` (`scene.framing`, `result.scene.chart`) and `calculateSubjectDistanceForFraming`.
|
|
86
|
+
*/
|
|
87
|
+
export function calculateChartFraming(input) {
|
|
88
|
+
const { record, sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions } = resolveFramingInput(input, "calculateChartFraming", ["subjectDistanceM", "focusDistanceM"]);
|
|
89
|
+
const subjectDistanceM = positive(record.subjectDistanceM, "subjectDistanceM");
|
|
90
|
+
const focusDistanceM = record.focusDistanceM === undefined ? subjectDistanceM : positive(record.focusDistanceM, "focusDistanceM");
|
|
91
|
+
if (record.focusDistanceM === undefined)
|
|
92
|
+
assumptions.push({ id: "focusDistanceM", kind: "default", value: subjectDistanceM, explanation: "The lens is focused on the chart plane." });
|
|
93
|
+
const f = focalLengthMm / 1000;
|
|
94
|
+
if (!(subjectDistanceM > f) || !(focusDistanceM > f))
|
|
95
|
+
throw new InvalidConfigurationError("subjectDistanceM and focusDistanceM must be more than the focal length (" + f + " m).");
|
|
96
|
+
// Written exactly as simulateCapture forms it, so the two agree to the last bit.
|
|
97
|
+
const imageDistanceMm = focalLengthMm * focusDistanceM * 1000 / (focusDistanceM * 1000 - focalLengthMm);
|
|
98
|
+
const magnification = imageDistanceMm / (subjectDistanceM * 1000);
|
|
99
|
+
const framedWidthM = sensorW * subjectDistanceM / imageDistanceMm, framedHeightM = sensorH * subjectDistanceM / imageDistanceMm;
|
|
100
|
+
const minimumFocusDistanceM = prime ? prime.minimumFocusDistanceM : calculateGenericPrimeMinimumFocusDistanceM(focalLengthMm);
|
|
101
|
+
assumptions.push({ id: "model.thin-lens-framing", kind: "model", value: { magnification, imageDistanceMm },
|
|
102
|
+
explanation: "Thin lens: image distance v = f·s/(s − f) at the focus distance s, magnification m = v/d on the chart plane at d, and the frame spans sensor side / m, " +
|
|
103
|
+
"the same framing simulateCapture uses. Distances are from the lens; pupil positions, focus breathing and distortion are not modelled." });
|
|
104
|
+
assumptions.push({ id: "minimumFocus", kind: "model", value: { minimumFocusDistanceM, rule: "max(0.10 m, f·(1 + 1/0.15))" },
|
|
105
|
+
explanation: "The generic prime family's minimum focus rule (a thin-lens approximation, not a real lens)" + (prime ? " for " + prime.lensId : " applied to this focal length") + "." });
|
|
106
|
+
if (record.equivalentFocalLengthMm !== undefined)
|
|
107
|
+
assumptions.push({ id: "focalLengthMm", kind: "derived", value: focalLengthMm,
|
|
108
|
+
explanation: "Equivalent focal length ÷ diagonal crop factor " + crop + " (calculateEquivalentFocalLength35Mm, 36 × 24 mm reference)." });
|
|
109
|
+
return Object.freeze({ subjectDistanceM, focusDistanceM, imageDistanceMm, magnification, focalLengthMm, equivalentFocalLengthMm: equivalent, cropFactor35Mm: crop,
|
|
110
|
+
chart: { widthM, heightM }, framedWidthM, framedHeightM, coverage: { frameWidthFraction: widthM / framedWidthM, frameHeightFraction: heightM / framedHeightM },
|
|
111
|
+
chartImageMm: { width: widthM * 1000 * magnification, height: heightM * 1000 * magnification }, ...closeUpLight(focalLengthMm, focusDistanceM), prime, minimumFocusDistanceM,
|
|
112
|
+
belowMinimumFocus: subjectDistanceM < minimumFocusDistanceM || focusDistanceM < minimumFocusDistanceM, scientificStatus: "approximation",
|
|
113
|
+
assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });
|
|
114
|
+
}
|
|
66
115
|
//# sourceMappingURL=subject-framing.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"subject-framing.js","sourceRoot":"","sources":["../../src/facade/subject-framing.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Subject distance for framing a chart (2.3). Thin lens: an object at distance d from a lens of focal length f\n * images at v = f·d/(d − f) with magnification m = v/d = f/(d − f), so the distance for a magnification m is\n * d = f·(1 + 1/m). This is the same framing the capture facade uses (`scene.framing`, `chartSize`), so a chart\n * placed at the returned distance with `chartSize` fills the frame as reported.\n * @see docs/QUICK_START.md, \"Fitting a chart to the frame\".\n */\n\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateEquivalentFocalLength35Mm } from \"../camera/equivalent-focal-length.js\";\nimport { calculateGenericPrimeMinimumFocusDistanceM, resolveGenericPrimeLens, GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM, type GenericPrimeLens } from \"../equipment/generic-prime-lenses.js\";\nimport type { SimulateCaptureAssumption } from \"./simulate-capture.js\";\n\n/** How the chart fits the frame: `contain` (default; the whole chart is visible and its limiting side touches the frame), `width`, `height` or `cover` (the chart fills the frame and is cropped on its other side). */\nexport type SubjectFramingFit = \"contain\" | \"width\" | \"height\" | \"cover\";\n\nexport interface CalculateSubjectDistanceForFramingInput {\n /** Active sensor (or simulated frame) size, mm. */\n sensor: { widthMm: number; heightMm: number };\n /** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */\n focalLengthMm?: number;\n /** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens. */\n equivalentFocalLengthMm?: number;\n /** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */\n chart: { widthM: number; heightM?: number };\n fit?: SubjectFramingFit;\n}\n\nexport interface SubjectFraming {\n /** Distance from the lens to the chart plane, metres: f·(1 + 1/m). */\n subjectDistanceM: number;\n /** Image size ÷ object size, f/(d − f). */\n magnification: number;\n /** Thin-lens image distance f·(1 + m), mm. */\n imageDistanceMm: number;\n focalLengthMm: number;\n equivalentFocalLengthMm: number;\n cropFactor35Mm: number;\n chart: { widthM: number; heightM: number };\n fit: SubjectFramingFit;\n /** Plane area the frame covers at that distance, metres. */\n framedWidthM: number;\n framedHeightM: number;\n /** Fraction of the frame width and height the chart covers (1 on the fitted side; above 1 means cropped). */\n coverage: { frameWidthFraction: number; frameHeightFraction: number };\n /** The generic prime of this equivalent focal length, when there is one. */\n prime: GenericPrimeLens | null;\n /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */\n minimumFocusDistanceM: number;\n /** True when the fit distance is closer than the minimum focus distance: the chart cannot be framed this way. */\n belowMinimumFocus: boolean;\n scientificStatus: \"approximation\";\n assumptions: readonly SimulateCaptureAssumption[];\n}\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0 || value > 1e7) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3). */\nexport function calculateSubjectDistanceForFraming(input: CalculateSubjectDistanceForFramingInput): SubjectFraming {\n requireInputObject(input, \"calculateSubjectDistanceForFraming input\");\n for (const key of Object.keys(input)) if (![\"sensor\", \"focalLengthMm\", \"equivalentFocalLengthMm\", \"chart\", \"fit\"].includes(key))\n throw new InvalidConfigurationError(\"calculateSubjectDistanceForFraming input has unknown field \\\"\" + key + \"\\\".\");\n requireInputObject(input.sensor, \"sensor\");\n requireInputObject(input.chart, \"chart\");\n for (const key of Object.keys(input.sensor)) if (key !== \"widthMm\" && key !== \"heightMm\") throw new InvalidConfigurationError(\"Unknown sensor field: \" + key + \".\");\n for (const key of Object.keys(input.chart)) if (key !== \"widthM\" && key !== \"heightM\") throw new InvalidConfigurationError(\"Unknown chart field: \" + key + \".\");\n const sensorW = positive(input.sensor.widthMm, \"sensor.widthMm\"), sensorH = positive(input.sensor.heightMm, \"sensor.heightMm\");\n if ((input.focalLengthMm === undefined) === (input.equivalentFocalLengthMm === undefined)) throw new InvalidConfigurationError(\"Give exactly one of focalLengthMm and equivalentFocalLengthMm.\");\n const crop = calculateEquivalentFocalLength35Mm({ focalLengthMm: 1, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }).value.cropFactor35Mm;\n const assumptions: SimulateCaptureAssumption[] = [];\n const equivalent = input.equivalentFocalLengthMm === undefined ? positive(input.focalLengthMm, \"focalLengthMm\") * crop : positive(input.equivalentFocalLengthMm, \"equivalentFocalLengthMm\");\n const focalLengthMm = input.focalLengthMm === undefined ? equivalent / crop : input.focalLengthMm;\n const prime = (GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM as readonly number[]).includes(equivalent) && input.equivalentFocalLengthMm !== undefined ?\n resolveGenericPrimeLens({ equivalentFocalLengthMm: equivalent, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }) : null;\n const widthM = positive(input.chart.widthM, \"chart.widthM\");\n const heightM = input.chart.heightM === undefined ? widthM * sensorH / sensorW : positive(input.chart.heightM, \"chart.heightM\");\n if (input.chart.heightM === undefined) assumptions.push({ id: \"chart.heightM\", kind: \"default\", value: heightM, explanation: \"The chart has the sensor's shape: height = width × sensor height / sensor width.\" });\n const fit = input.fit ?? \"contain\";\n if (![\"contain\", \"width\", \"height\", \"cover\"].includes(fit)) throw new InvalidConfigurationError(\"fit must be contain, width, height or cover.\");\n if (input.fit === undefined) assumptions.push({ id: \"fit\", kind: \"default\", value: \"contain\", explanation: \"The whole chart is visible: the side whose chart-to-sensor ratio is larger (the limiting side) fills the frame.\" });\n // Magnification that makes each chart side exactly fill the matching sensor side.\n const mWidth = sensorW / (widthM * 1000), mHeight = sensorH / (heightM * 1000);\n const m = fit === \"width\" ? mWidth : fit === \"height\" ? mHeight : fit === \"contain\" ? Math.min(mWidth, mHeight) : Math.max(mWidth, mHeight);\n const f = focalLengthMm / 1000, subjectDistanceM = f * (1 + 1 / m), minimumFocusDistanceM = prime ? prime.minimumFocusDistanceM : calculateGenericPrimeMinimumFocusDistanceM(focalLengthMm);\n assumptions.push({ id: \"model.thin-lens-framing\", kind: \"model\", value: { magnification: m, subjectDistanceM },\n explanation: \"Thin lens focused on the chart plane: d = f·(1 + 1/m), image distance v = f·(1 + m), the same framing simulateCapture uses. Distances are from the lens; pupil positions, focus breathing and distortion are not modelled.\" });\n assumptions.push({ id: \"minimumFocus\", kind: \"model\", value: { minimumFocusDistanceM, rule: \"max(0.10 m, f·(1 + 1/0.15))\" },\n explanation: \"The generic prime family's minimum focus rule (a thin-lens approximation, not a real lens)\" + (prime ? \" for \" + prime.lensId : \" applied to this focal length\") + \".\" });\n if (input.equivalentFocalLengthMm !== undefined) assumptions.push({ id: \"focalLengthMm\", kind: \"derived\", value: focalLengthMm,\n explanation: \"Equivalent focal length ÷ diagonal crop factor \" + crop + \" (calculateEquivalentFocalLength35Mm, 36 × 24 mm reference).\" });\n return Object.freeze({ subjectDistanceM, magnification: m, imageDistanceMm: focalLengthMm * (1 + m), focalLengthMm, equivalentFocalLengthMm: equivalent, cropFactor35Mm: crop,\n chart: { widthM, heightM }, fit, framedWidthM: sensorW / 1000 / m, framedHeightM: sensorH / 1000 / m,\n coverage: { frameWidthFraction: widthM * 1000 * m / sensorW, frameHeightFraction: heightM * 1000 * m / sensorH }, prime, minimumFocusDistanceM,\n belowMinimumFocus: subjectDistanceM < minimumFocusDistanceM, scientificStatus: \"approximation\" as const, assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });\n}\n"]}
|
|
1
|
+
{"version":3,"file":"subject-framing.js","sourceRoot":"","sources":["../../src/facade/subject-framing.ts"],"names":[],"mappings":"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","sourcesContent":["// SPDX-License-Identifier: Apache-2.0\n\n/**\n * Subject distance for framing a chart (2.3). Thin lens: an object at distance d from a lens of focal length f\n * images at v = f·d/(d − f) with magnification m = v/d = f/(d − f), so the distance for a magnification m is\n * d = f·(1 + 1/m). This is the same framing the capture facade uses (`scene.framing`, `chartSize`), so a chart\n * placed at the returned distance with `chartSize` fills the frame as reported.\n *\n * 2.5: `calculateChartFraming` is the inverse question: at a given subject distance (and focus distance), how much of the\n * frame does the chart cover? It uses the facade's own framing, m = v/d with v = f·s/(s − f) at the focus distance s.\n * @see docs/QUICK_START.md, \"Fitting a chart to the frame\" and \"Chart coverage at any distance (2.5)\".\n */\n\nimport { thinLensWorkingFNumberFactor } from \"../optics/scene-to-sensor-irradiance.js\";\nimport { InvalidConfigurationError } from \"../core/configuration-error.js\";\nimport { requireInputObject } from \"../core/record-validation.js\";\nimport { calculateEquivalentFocalLength35Mm } from \"../camera/equivalent-focal-length.js\";\nimport { calculateGenericPrimeMinimumFocusDistanceM, resolveGenericPrimeLens, GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM, type GenericPrimeLens } from \"../equipment/generic-prime-lenses.js\";\nimport type { SimulateCaptureAssumption } from \"./simulate-capture.js\";\n\n/** How the chart fits the frame: `contain` (default; the whole chart is visible and its limiting side touches the frame), `width`, `height` or `cover` (the chart fills the frame and is cropped on its other side). */\nexport type SubjectFramingFit = \"contain\" | \"width\" | \"height\" | \"cover\";\n\nexport interface CalculateSubjectDistanceForFramingInput {\n /** Active sensor (or simulated frame) size, mm. */\n sensor: { widthMm: number; heightMm: number };\n /** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */\n focalLengthMm?: number;\n /** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens. */\n equivalentFocalLengthMm?: number;\n /** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */\n chart: { widthM: number; heightM?: number };\n fit?: SubjectFramingFit;\n}\n\nexport interface SubjectFraming {\n /** Distance from the lens to the chart plane, metres: f·(1 + 1/m). */\n subjectDistanceM: number;\n /** Image size ÷ object size, f/(d − f). */\n magnification: number;\n /** Thin-lens image distance f·(1 + m), mm. */\n imageDistanceMm: number;\n focalLengthMm: number;\n equivalentFocalLengthMm: number;\n cropFactor35Mm: number;\n chart: { widthM: number; heightM: number };\n fit: SubjectFramingFit;\n /** Plane area the frame covers at that distance, metres. */\n framedWidthM: number;\n framedHeightM: number;\n /** Fraction of the frame width and height the chart covers (1 on the fitted side; above 1 means cropped). */\n coverage: { frameWidthFraction: number; frameHeightFraction: number };\n /**\n * 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance\n * relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor\n * `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).\n */\n closeUpTransmission: number;\n /** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */\n closeUpLossStops: number;\n /** The generic prime of this equivalent focal length, when there is one. */\n prime: GenericPrimeLens | null;\n /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */\n minimumFocusDistanceM: number;\n /** True when the fit distance is closer than the minimum focus distance: the chart cannot be framed this way. */\n belowMinimumFocus: boolean;\n scientificStatus: \"approximation\";\n assumptions: readonly SimulateCaptureAssumption[];\n}\n\nfunction positive(value: unknown, name: string): number {\n if (typeof value !== \"number\" || !Number.isFinite(value) || value <= 0 || value > 1e7) throw new InvalidConfigurationError(name + \" must be a positive finite number.\");\n return value;\n}\n\n/** Shared input checks and lens resolution of the two framing functions. */\nfunction resolveFramingInput(input: unknown, name: string, extraKeys: readonly string[]): { record: Record<string, unknown>; sensorW: number; sensorH: number; crop: number;\n equivalent: number; focalLengthMm: number; prime: GenericPrimeLens | null; widthM: number; heightM: number; assumptions: SimulateCaptureAssumption[] } {\n requireInputObject(input, name + \" input\");\n const record = input as Record<string, unknown>;\n for (const key of Object.keys(record)) if (![\"sensor\", \"focalLengthMm\", \"equivalentFocalLengthMm\", \"chart\", ...extraKeys].includes(key))\n throw new InvalidConfigurationError(name + \" input has unknown field \\\"\" + key + \"\\\".\");\n requireInputObject(record.sensor, \"sensor\");\n requireInputObject(record.chart, \"chart\");\n const sensor = record.sensor as Record<string, unknown>, chart = record.chart as Record<string, unknown>;\n for (const key of Object.keys(sensor)) if (key !== \"widthMm\" && key !== \"heightMm\") throw new InvalidConfigurationError(\"Unknown sensor field: \" + key + \".\");\n for (const key of Object.keys(chart)) if (key !== \"widthM\" && key !== \"heightM\") throw new InvalidConfigurationError(\"Unknown chart field: \" + key + \".\");\n const sensorW = positive(sensor.widthMm, \"sensor.widthMm\"), sensorH = positive(sensor.heightMm, \"sensor.heightMm\");\n if ((record.focalLengthMm === undefined) === (record.equivalentFocalLengthMm === undefined)) throw new InvalidConfigurationError(\"Give exactly one of focalLengthMm and equivalentFocalLengthMm.\");\n const crop = calculateEquivalentFocalLength35Mm({ focalLengthMm: 1, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }).value.cropFactor35Mm;\n const assumptions: SimulateCaptureAssumption[] = [];\n const equivalent = record.equivalentFocalLengthMm === undefined ? positive(record.focalLengthMm, \"focalLengthMm\") * crop : positive(record.equivalentFocalLengthMm, \"equivalentFocalLengthMm\");\n const focalLengthMm = record.focalLengthMm === undefined ? equivalent / crop : record.focalLengthMm as number;\n const prime = (GENERIC_PRIME_EQUIVALENT_FOCAL_LENGTHS_MM as readonly number[]).includes(equivalent) && record.equivalentFocalLengthMm !== undefined ?\n resolveGenericPrimeLens({ equivalentFocalLengthMm: equivalent, activeImagingArea: { widthMm: sensorW, heightMm: sensorH } }) : null;\n const widthM = positive(chart.widthM, \"chart.widthM\");\n const heightM = chart.heightM === undefined ? widthM * sensorH / sensorW : positive(chart.heightM, \"chart.heightM\");\n if (chart.heightM === undefined) assumptions.push({ id: \"chart.heightM\", kind: \"default\", value: heightM, explanation: \"The chart has the sensor's shape: height = width × sensor height / sensor width.\" });\n return { record, sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions };\n}\n\n/** The subject distance that fits a chart of a given physical size to the frame, with magnification, coverage and the minimum-focus check (2.3). */\nexport function calculateSubjectDistanceForFraming(input: CalculateSubjectDistanceForFramingInput): SubjectFraming {\n const { sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions } = resolveFramingInput(input, \"calculateSubjectDistanceForFraming\", [\"fit\"]);\n const fit = input.fit ?? \"contain\";\n if (![\"contain\", \"width\", \"height\", \"cover\"].includes(fit)) throw new InvalidConfigurationError(\"fit must be contain, width, height or cover.\");\n if (input.fit === undefined) assumptions.push({ id: \"fit\", kind: \"default\", value: \"contain\", explanation: \"The whole chart is visible: the side whose chart-to-sensor ratio is larger (the limiting side) fills the frame.\" });\n // Magnification that makes each chart side exactly fill the matching sensor side.\n const mWidth = sensorW / (widthM * 1000), mHeight = sensorH / (heightM * 1000);\n const m = fit === \"width\" ? mWidth : fit === \"height\" ? mHeight : fit === \"contain\" ? Math.min(mWidth, mHeight) : Math.max(mWidth, mHeight);\n const f = focalLengthMm / 1000, subjectDistanceM = f * (1 + 1 / m), minimumFocusDistanceM = prime ? prime.minimumFocusDistanceM : calculateGenericPrimeMinimumFocusDistanceM(focalLengthMm);\n assumptions.push({ id: \"model.thin-lens-framing\", kind: \"model\", value: { magnification: m, subjectDistanceM },\n explanation: \"Thin lens focused on the chart plane: d = f·(1 + 1/m), image distance v = f·(1 + m), the same framing simulateCapture uses. Distances are from the lens; pupil positions, focus breathing and distortion are not modelled.\" });\n assumptions.push({ id: \"minimumFocus\", kind: \"model\", value: { minimumFocusDistanceM, rule: \"max(0.10 m, f·(1 + 1/0.15))\" },\n explanation: \"The generic prime family's minimum focus rule (a thin-lens approximation, not a real lens)\" + (prime ? \" for \" + prime.lensId : \" applied to this focal length\") + \".\" });\n if (input.equivalentFocalLengthMm !== undefined) assumptions.push({ id: \"focalLengthMm\", kind: \"derived\", value: focalLengthMm,\n explanation: \"Equivalent focal length ÷ diagonal crop factor \" + crop + \" (calculateEquivalentFocalLength35Mm, 36 × 24 mm reference).\" });\n return Object.freeze({ subjectDistanceM, magnification: m, imageDistanceMm: focalLengthMm * (1 + m), focalLengthMm, equivalentFocalLengthMm: equivalent, cropFactor35Mm: crop,\n chart: { widthM, heightM }, fit, framedWidthM: sensorW / 1000 / m, framedHeightM: sensorH / 1000 / m,\n coverage: { frameWidthFraction: widthM * 1000 * m / sensorW, frameHeightFraction: heightM * 1000 * m / sensorH }, ...closeUpLight(focalLengthMm, subjectDistanceM), prime, minimumFocusDistanceM,\n belowMinimumFocus: subjectDistanceM < minimumFocusDistanceM, scientificStatus: \"approximation\" as const, assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });\n}\n\n/** 2.6: the close-up light factor at a focus distance, from the irradiance bridge's own working f-number factor. */\nfunction closeUpLight(focalLengthMm: number, focusDistanceM: number): { closeUpTransmission: number; closeUpLossStops: number } {\n const factor = thinLensWorkingFNumberFactor(focalLengthMm, focusDistanceM);\n return { closeUpTransmission: 1 / (factor * factor), closeUpLossStops: 2 * Math.log2(factor) };\n}\n\nexport interface CalculateChartFramingInput {\n /** Active sensor (or simulated frame) size, mm: for `simulateCapture`, output width × height × pixel pitch. */\n sensor: { widthMm: number; heightMm: number };\n /** Physical focal length, mm. Give exactly one of this and `equivalentFocalLengthMm`. */\n focalLengthMm?: number;\n /** Full-frame-equivalent focal length (diagonal crop factor); a generic prime's value also reports that lens (as `lens.prime`). */\n equivalentFocalLengthMm?: number;\n /** Chart size as for `simulateCapture` `chartSize`: heightM defaults to widthM × the sensor aspect (height / width). */\n chart: { widthM: number; heightM?: number };\n /** Distance from the lens to the chart plane, metres; more than the focal length. */\n subjectDistanceM: number;\n /** Focus distance, metres (as `lens.focusDistanceM`); default the subject distance. More than the focal length. */\n focusDistanceM?: number;\n}\n\nexport interface ChartFraming {\n subjectDistanceM: number;\n focusDistanceM: number;\n /** Thin-lens image distance at the focus distance, f·s/(s − f), mm. */\n imageDistanceMm: number;\n /** Image size ÷ object size on the chart plane, v/d: f/(d − f) when focused on the chart. */\n magnification: number;\n focalLengthMm: number;\n equivalentFocalLengthMm: number;\n cropFactor35Mm: number;\n chart: { widthM: number; heightM: number };\n /** Plane area the frame covers at the subject distance, metres: sensor side ÷ magnification. */\n framedWidthM: number;\n framedHeightM: number;\n /** Fraction of the frame width and height the chart covers; above 1 means the chart is larger than the frame and cropped. */\n coverage: { frameWidthFraction: number; frameHeightFraction: number };\n /** The chart's image on the sensor, mm. */\n chartImageMm: { width: number; height: number };\n /**\n * 2.6: the thin-lens close-up light factor at the focus distance, 1/(1 + m)² with m = v/s: the image-plane irradiance\n * relative to the same lens and f-number focused at infinity (working f-number N·(1 + m)). It is the factor\n * `simulateCapture` applies through the scene-to-sensor irradiance (same function, same value).\n */\n closeUpTransmission: number;\n /** 2.6: the same factor in stops, 2·log₂(1 + m): how much darker the image is than at infinity focus. */\n closeUpLossStops: number;\n /** The generic prime of this equivalent focal length, when there is one. */\n prime: GenericPrimeLens | null;\n /** The generic prime rule max(0.10 m, f·(1 + 1/0.15)) at this focal length (the prime's own value when there is one). */\n minimumFocusDistanceM: number;\n /** True when the subject or focus distance is closer than the minimum focus distance (`simulateCapture` with `lens.prime` rejects it). */\n belowMinimumFocus: boolean;\n scientificStatus: \"approximation\";\n assumptions: readonly SimulateCaptureAssumption[];\n}\n\n/**\n * How much of the frame a chart of a given physical size covers at any subject distance (2.5): magnification, framed plane size,\n * coverage fractions (above 1 when cropped), the chart's image size and the minimum-focus check. The same thin-lens framing as\n * `simulateCapture` (`scene.framing`, `result.scene.chart`) and `calculateSubjectDistanceForFraming`.\n */\nexport function calculateChartFraming(input: CalculateChartFramingInput): ChartFraming {\n const { record, sensorW, sensorH, crop, equivalent, focalLengthMm, prime, widthM, heightM, assumptions } = resolveFramingInput(input, \"calculateChartFraming\",\n [\"subjectDistanceM\", \"focusDistanceM\"]);\n const subjectDistanceM = positive(record.subjectDistanceM, \"subjectDistanceM\");\n const focusDistanceM = record.focusDistanceM === undefined ? subjectDistanceM : positive(record.focusDistanceM, \"focusDistanceM\");\n if (record.focusDistanceM === undefined) assumptions.push({ id: \"focusDistanceM\", kind: \"default\", value: subjectDistanceM, explanation: \"The lens is focused on the chart plane.\" });\n const f = focalLengthMm / 1000;\n if (!(subjectDistanceM > f) || !(focusDistanceM > f)) throw new InvalidConfigurationError(\"subjectDistanceM and focusDistanceM must be more than the focal length (\" + f + \" m).\");\n // Written exactly as simulateCapture forms it, so the two agree to the last bit.\n const imageDistanceMm = focalLengthMm * focusDistanceM * 1000 / (focusDistanceM * 1000 - focalLengthMm);\n const magnification = imageDistanceMm / (subjectDistanceM * 1000);\n const framedWidthM = sensorW * subjectDistanceM / imageDistanceMm, framedHeightM = sensorH * subjectDistanceM / imageDistanceMm;\n const minimumFocusDistanceM = prime ? prime.minimumFocusDistanceM : calculateGenericPrimeMinimumFocusDistanceM(focalLengthMm);\n assumptions.push({ id: \"model.thin-lens-framing\", kind: \"model\", value: { magnification, imageDistanceMm },\n explanation: \"Thin lens: image distance v = f·s/(s − f) at the focus distance s, magnification m = v/d on the chart plane at d, and the frame spans sensor side / m, \" +\n \"the same framing simulateCapture uses. Distances are from the lens; pupil positions, focus breathing and distortion are not modelled.\" });\n assumptions.push({ id: \"minimumFocus\", kind: \"model\", value: { minimumFocusDistanceM, rule: \"max(0.10 m, f·(1 + 1/0.15))\" },\n explanation: \"The generic prime family's minimum focus rule (a thin-lens approximation, not a real lens)\" + (prime ? \" for \" + prime.lensId : \" applied to this focal length\") + \".\" });\n if (record.equivalentFocalLengthMm !== undefined) assumptions.push({ id: \"focalLengthMm\", kind: \"derived\", value: focalLengthMm,\n explanation: \"Equivalent focal length ÷ diagonal crop factor \" + crop + \" (calculateEquivalentFocalLength35Mm, 36 × 24 mm reference).\" });\n return Object.freeze({ subjectDistanceM, focusDistanceM, imageDistanceMm, magnification, focalLengthMm, equivalentFocalLengthMm: equivalent, cropFactor35Mm: crop,\n chart: { widthM, heightM }, framedWidthM, framedHeightM, coverage: { frameWidthFraction: widthM / framedWidthM, frameHeightFraction: heightM / framedHeightM },\n chartImageMm: { width: widthM * 1000 * magnification, height: heightM * 1000 * magnification }, ...closeUpLight(focalLengthMm, focusDistanceM), prime, minimumFocusDistanceM,\n belowMinimumFocus: subjectDistanceM < minimumFocusDistanceM || focusDistanceM < minimumFocusDistanceM, scientificStatus: \"approximation\" as const,\n assumptions: Object.freeze(assumptions.map((a) => Object.freeze(a))) });\n}\n"]}
|