@photivra/engine 2.8.0 → 2.8.1
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 +33 -5
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/dist/capture/export-jpeg.js +25 -12
- package/dist/capture/export-jpeg.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/diffraction-provider.js +3 -10
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/expected-signal-setup.d.ts +1 -1
- package/dist/facade/expected-signal-setup.js +2 -6
- package/dist/facade/expected-signal-setup.js.map +1 -1
- package/dist/facade/simulate-capture.js +49 -17
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/optics/site-diffraction-kernel.d.ts +5 -0
- package/dist/optics/site-diffraction-kernel.js +32 -0
- package/dist/optics/site-diffraction-kernel.js.map +1 -0
- package/dist/output/print-jpeg.js +70 -42
- package/dist/output/print-jpeg.js.map +1 -1
- package/docs/API_REFERENCE.md +1 -1
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +2 -2
- package/package.json +1 -1
|
@@ -74,7 +74,7 @@ import { calculateSensorSpectralQuadrature } from "../sensor/spectral-quadrature
|
|
|
74
74
|
import { createDiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
75
75
|
import { createDiffractedPlanarProvider } from "./diffraction-provider.js";
|
|
76
76
|
import { resolveGenericPrimeLens } from "../equipment/generic-prime-lenses.js";
|
|
77
|
-
import {
|
|
77
|
+
import { executeEnvironmentSensorPhotoSignal, planEnvironmentSensorPhotoSignal } from "../sensor/environment-photo-signal.js";
|
|
78
78
|
import { calculateSiteEffectiveSaturationCode } from "../capture/raw-white-level.js";
|
|
79
79
|
import { prepareSensorColorDevelopment } from "../color/sensor-color-development.js";
|
|
80
80
|
import { resolveAntiAliasingSpatialKernel } from "../sensor/optical-stack.js";
|
|
@@ -142,6 +142,9 @@ const AHD_VALUE_REACH = 3;
|
|
|
142
142
|
/** Anti-aliasing filter limits: split points, and the largest offset in sites (bounds the scene work and the convolution margin). */
|
|
143
143
|
const AA_MAXIMUM_COMPONENTS = 16;
|
|
144
144
|
const AA_MAXIMUM_OFFSET_SITES = 4;
|
|
145
|
+
/** Channel responses kept across calls (`axisChannelResponse`): one entry per distinct sensor, exposure and optics state. */
|
|
146
|
+
const CHANNEL_RESPONSE_CACHE_SIZE = 8;
|
|
147
|
+
const channelResponseCache = new Map();
|
|
145
148
|
/** Default birefringent separation in sites: the two-spot MTF |cos(π·f·d)| then has its first zero at Nyquist, f = 1/(2·pitch). */
|
|
146
149
|
const AA_DEFAULT_SEPARATION_SITES = 1;
|
|
147
150
|
/** Parses `sensor.antiAliasing` into engine point-splitting components (offsets in µm, +x right, +y down). */
|
|
@@ -1313,13 +1316,16 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1313
1316
|
const yieldControl = createHostYield().yieldControl;
|
|
1314
1317
|
const perSite = queriesPerSite * (diffraction && psf === diffractionPsf ? (2 * diffraction.kernelRadiusSamples + 1) ** 2 : 1);
|
|
1315
1318
|
let diffractionMs = 0, demosaicMs, encodeMs;
|
|
1316
|
-
const
|
|
1319
|
+
const diffractedProvider = (window) => diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
1317
1320
|
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
1318
1321
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
1319
1322
|
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
1320
|
-
...(
|
|
1323
|
+
...(window ? { window } : {}),
|
|
1321
1324
|
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
1322
1325
|
...(antiAliasing || boxSamples > 1 ? { queryOffsetsSites: apertureNodeOffsets(boxSamples).flatMap((node) => (antiAliasing ? antiAliasing.components : [{ offsetMicrometers: { x: 0, y: 0 } }]).map((c) => ({ x: node.x - c.offsetMicrometers.x / pitch, y: node.y - c.offsetMicrometers.y / pitch }))) } : {}) }) : provider;
|
|
1326
|
+
// Built on first use: a capture that takes the tabulated path (#394 step 2) never queries it, and its kernels cost a setup.
|
|
1327
|
+
let rawProviderBuilt;
|
|
1328
|
+
const rawProvider = () => (rawProviderBuilt ??= diffractedProvider(regionNative));
|
|
1323
1329
|
if (antiAliasing && perSite > 100_000)
|
|
1324
1330
|
fail("sensor.antiAliasing with these options needs " + perSite + " scene queries per site; the environment admits at most 100000.");
|
|
1325
1331
|
const evaluations = nativeWidth * nativeHeight * perSite;
|
|
@@ -1335,7 +1341,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1335
1341
|
// is linear in the radiance node values and, for this planar scene, the same at every site (paraxial π/(4N_w²)).
|
|
1336
1342
|
const cx = Math.floor(nativeWidth / 2) & ~1, cy = Math.floor(nativeHeight / 2) & ~1;
|
|
1337
1343
|
const channelSites = { red: cy * nativeWidth + cx, green: cy * nativeWidth + Math.min(cx + 1, nativeWidth - 1), blue: Math.min(cy + 1, nativeHeight - 1) * nativeWidth + Math.min(cx + 1, nativeWidth - 1) };
|
|
1338
|
-
const
|
|
1344
|
+
const environments = {};
|
|
1339
1345
|
for (const channel of CHANNELS) {
|
|
1340
1346
|
if (channelAt(channelSites[channel] % nativeWidth, Math.floor(channelSites[channel] / nativeWidth)) !== channel)
|
|
1341
1347
|
fail(caller + " needs at least a 2 × 2 native raster.");
|
|
@@ -1346,13 +1352,36 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1346
1352
|
environment.temporalSampleCount = 1;
|
|
1347
1353
|
delete environment.pupil;
|
|
1348
1354
|
delete environment.lensFieldMapping;
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1355
|
+
delete environment.evaluateRadiance;
|
|
1356
|
+
delete environment.evaluateApertureRadiance;
|
|
1357
|
+
environments[channel] = environment;
|
|
1358
|
+
}
|
|
1359
|
+
// The responses are a function of these records alone (the radiance is replaced per node), and a request split into region
|
|
1360
|
+
// tiles builds the same records for every tile, so they are kept across calls (the last CHANNEL_RESPONSE_CACHE_SIZE).
|
|
1361
|
+
const key = JSON.stringify(CHANNELS.map((c) => environments[c]));
|
|
1362
|
+
const copy = (responses) => Object.fromEntries(CHANNELS.map((c) => [c, new Map(responses[c])]));
|
|
1363
|
+
const cached = channelResponseCache.get(key);
|
|
1364
|
+
let electronsPerRadiance;
|
|
1365
|
+
if (cached) {
|
|
1366
|
+
channelResponseCache.delete(key);
|
|
1367
|
+
channelResponseCache.set(key, cached);
|
|
1368
|
+
electronsPerRadiance = copy(cached);
|
|
1369
|
+
}
|
|
1370
|
+
else {
|
|
1371
|
+
electronsPerRadiance = {};
|
|
1372
|
+
for (const channel of CHANNELS) {
|
|
1373
|
+
// Planned once per channel and executed per spectral node: calculateEnvironmentSensorPhotoSignal is this plan and execution.
|
|
1374
|
+
const photoPlan = planEnvironmentSensorPhotoSignal(environments[channel]);
|
|
1375
|
+
const electrons = (radiance) => executeEnvironmentSensorPhotoSignal(photoPlan, (request) => ({ ...provider.evaluateRadiance(request), spectralRadianceWattsPerSquareMeterSteradianNanometer: radiance(request.wavelengthNanometers) })).value
|
|
1376
|
+
.photo.value.photoSignal.expectedGeneratedElectronCount;
|
|
1377
|
+
const nodes = new Set();
|
|
1378
|
+
electrons((nm) => { nodes.add(nm); return 1; });
|
|
1379
|
+
electronsPerRadiance[channel] = new Map([...nodes].sort((a, b) => a - b).map((node) => [node, electrons((nm) => (nm === node ? 1 : 0))]));
|
|
1380
|
+
}
|
|
1381
|
+
// Callers get their own maps; the cached ones are never handed out.
|
|
1382
|
+
channelResponseCache.set(key, copy(electronsPerRadiance));
|
|
1383
|
+
if (channelResponseCache.size > CHANNEL_RESPONSE_CACHE_SIZE)
|
|
1384
|
+
channelResponseCache.delete(channelResponseCache.keys().next().value);
|
|
1356
1385
|
}
|
|
1357
1386
|
const wavelengths = [...new Set(CHANNELS.flatMap((c) => [...electronsPerRadiance[c].keys()]))].sort((a, b) => a - b);
|
|
1358
1387
|
// 2. Electrons → normalized RAW signal: (code − black)/(white − black) = e·d/(G·(W − B)) for the linear readout
|
|
@@ -1482,8 +1511,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1482
1511
|
};
|
|
1483
1512
|
const rawTick = progress("raw", regionNative ? regionNative.height * Math.ceil(regionNative.width / tileWidth) : plan.tileCount);
|
|
1484
1513
|
// #394: siteInput builds fresh site records per call and never changes them, so the native task plans them without a copy.
|
|
1485
|
-
const rawCallbacks = { readTile: markInternalNativeTileReader(async (request) => { const tile = await readTile(request); rawTick(); return tile; }),
|
|
1486
|
-
evaluateRadiance: rawProvider.evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl };
|
|
1514
|
+
const rawCallbacks = () => ({ readTile: markInternalNativeTileReader(async (request) => { const tile = await readTile(request); rawTick(); return tile; }),
|
|
1515
|
+
evaluateRadiance: rawProvider().evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl });
|
|
1487
1516
|
// #394 step 2: an engine-built planar scene (chart preset or caller material scene; still, rectilinear, no distortion, vignetting,
|
|
1488
1517
|
// pupil rays or caller PSF) takes its expected electrons from the tabulated signal, verified per CFA phase against the chain.
|
|
1489
1518
|
const tabulatedScene = chart !== undefined || callerScene?.kind === "material-function";
|
|
@@ -1498,7 +1527,10 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1498
1527
|
list: [...sceneMaterials.map((m, index) => (m.kind === "neutral" ? { neutral: m.reflectance } : { radiance: (nm) => materialRadiance(index, nm) })),
|
|
1499
1528
|
...(chartSize ? [{ neutral: chartSize.surroundReflectance }] : [])] },
|
|
1500
1529
|
radianceAt: provider.radianceAt, imagingArea, focalLengthMm, focusDistanceM, subjectDistanceM, metresPerFrameUnit, caller: "simulateCapture" });
|
|
1501
|
-
|
|
1530
|
+
// The verification reads only the region's top-left 2 × 2 block, so its diffraction convolves only that block's lattice; the
|
|
1531
|
+
// window never changes a value (see diffraction-provider.ts), so the chain it runs is the whole render's.
|
|
1532
|
+
const verificationProvider = diffractedProvider({ x: regionNative?.x ?? 0, y: regionNative?.y ?? 0, width: 2, height: 2 });
|
|
1533
|
+
tabulated = await renderTabulatedNativeRaw({ input: environmentRaw, region: regionNative, siteInput, evaluateRadiance: verificationProvider.evaluateRadiance,
|
|
1502
1534
|
electrons: createDisplayViewExpectedSignal(signalInput), onTile: rawTick, tileWidth, yieldControl: () => yieldControl(), checkAbort });
|
|
1503
1535
|
}
|
|
1504
1536
|
let rawOutput;
|
|
@@ -1512,8 +1544,8 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1512
1544
|
"diagnostics.providerEvaluationCount counts those sites' evaluator calls and the table's scene lookups.");
|
|
1513
1545
|
}
|
|
1514
1546
|
else {
|
|
1515
|
-
const rawTask = regionNative ? createNativeEnvironmentRawRegionTask(environmentRaw, regionNative, rawCallbacks) :
|
|
1516
|
-
createNativeEnvironmentRawTask(environmentRaw, rawCallbacks);
|
|
1547
|
+
const rawTask = regionNative ? createNativeEnvironmentRawRegionTask(environmentRaw, regionNative, rawCallbacks()) :
|
|
1548
|
+
createNativeEnvironmentRawTask(environmentRaw, rawCallbacks());
|
|
1517
1549
|
await runTask(rawTask);
|
|
1518
1550
|
rawOutput = rawTask.takeOutput();
|
|
1519
1551
|
}
|
|
@@ -1562,7 +1594,7 @@ export async function runCapture(options, run, viewOption, renderView) {
|
|
|
1562
1594
|
// retained optical stage, and is faster: retaining the optical stage takes the first pass off the lean executor.
|
|
1563
1595
|
const wbTask = createNativeEnvironmentRawTask(finalRawInput, {
|
|
1564
1596
|
readTile: markInternalNativeTileReader(async (request) => { const tile = await readTile(request); wbTick(); return tile; }),
|
|
1565
|
-
evaluateRadiance: rawProvider.evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl
|
|
1597
|
+
evaluateRadiance: rawProvider().evaluateRadiance, ...(pupil ? { evaluateApertureRadiance: provider.evaluateApertureRadiance } : {}), yieldControl
|
|
1566
1598
|
});
|
|
1567
1599
|
await runTask(wbTask);
|
|
1568
1600
|
rawOutput = wbTask.takeOutput();
|