@photivra/engine 2.5.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +129 -1
- package/CITATION.cff +1 -1
- package/README.md +6 -6
- package/SECURITY.md +1 -1
- package/THIRD_PARTY.md +1 -0
- package/dist/capture/native-environment-lean.d.ts +1 -1
- package/dist/capture/native-environment-lean.js +17 -3
- package/dist/capture/native-environment-lean.js.map +1 -1
- package/dist/capture/native-environment-raw.d.ts +0 -4
- package/dist/capture/native-environment-raw.js +6 -2
- package/dist/capture/native-environment-raw.js.map +1 -1
- package/dist/color/illuminants.d.ts +15 -0
- package/dist/color/illuminants.js +81 -24
- package/dist/color/illuminants.js.map +1 -1
- package/dist/color/srgb-transfer.d.ts +6 -0
- package/dist/color/srgb-transfer.js +9 -0
- package/dist/color/srgb-transfer.js.map +1 -1
- package/dist/color/white-balance.d.ts +6 -0
- package/dist/color/white-balance.js +15 -2
- package/dist/color/white-balance.js.map +1 -1
- package/dist/core/internal-radiance-evaluator.d.ts +6 -0
- package/dist/core/internal-radiance-evaluator.js +33 -0
- package/dist/core/internal-radiance-evaluator.js.map +1 -0
- package/dist/core/portable-math.d.ts +33 -0
- package/dist/core/portable-math.js +342 -0
- package/dist/core/portable-math.js.map +1 -1
- package/dist/core/version.d.ts +1 -1
- package/dist/core/version.js +1 -1
- package/dist/core/version.js.map +1 -1
- package/dist/facade/chart-radiance-batch.js +3 -2
- package/dist/facade/chart-radiance-batch.js.map +1 -1
- package/dist/facade/chart-scenes.d.ts +11 -0
- package/dist/facade/chart-scenes.js +21 -7
- package/dist/facade/chart-scenes.js.map +1 -1
- package/dist/facade/diffraction-provider.d.ts +10 -0
- package/dist/facade/diffraction-provider.js +38 -17
- package/dist/facade/diffraction-provider.js.map +1 -1
- package/dist/facade/display-view.d.ts +213 -0
- package/dist/facade/display-view.js +1543 -0
- package/dist/facade/display-view.js.map +1 -0
- package/dist/facade/scene-radiance.d.ts +45 -5
- package/dist/facade/scene-radiance.js +97 -28
- package/dist/facade/scene-radiance.js.map +1 -1
- package/dist/facade/simulate-capture.d.ts +233 -5
- package/dist/facade/simulate-capture.js +120 -37
- package/dist/facade/simulate-capture.js.map +1 -1
- package/dist/facade/simulate-display-view.d.ts +45 -0
- package/dist/facade/simulate-display-view.js +214 -0
- package/dist/facade/simulate-display-view.js.map +1 -0
- package/dist/facade/subject-framing.d.ts +16 -0
- package/dist/facade/subject-framing.js +8 -2
- package/dist/facade/subject-framing.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/optics/diffraction-psf-profile.d.ts +9 -2
- package/dist/optics/diffraction-psf-profile.js +33 -4
- package/dist/optics/diffraction-psf-profile.js.map +1 -1
- package/dist/optics/irradiance-convolution.js +1 -1
- package/dist/optics/irradiance-convolution.js.map +1 -1
- package/dist/optics/scene-to-sensor-irradiance.d.ts +7 -0
- package/dist/optics/scene-to-sensor-irradiance.js +10 -5
- package/dist/optics/scene-to-sensor-irradiance.js.map +1 -1
- package/dist/output/print-plan.d.ts +8 -1
- package/dist/output/print-plan.js +8 -1
- package/dist/output/print-plan.js.map +1 -1
- package/dist/schema/scene-radiance.d.ts +16 -0
- package/dist/schema/scene-radiance.js +25 -4
- package/dist/schema/scene-radiance.js.map +1 -1
- package/dist/sensor/raw-readout.d.ts +12 -0
- package/dist/sensor/raw-readout.js +131 -9
- package/dist/sensor/raw-readout.js.map +1 -1
- package/dist/sensor/stochastic-seed.d.ts +13 -0
- package/dist/sensor/stochastic-seed.js +42 -8
- package/dist/sensor/stochastic-seed.js.map +1 -1
- package/docs/AGENT_GUIDE.md +1 -0
- package/docs/API_REFERENCE.md +19 -1
- package/docs/QUICK_START.md +203 -4
- package/docs/USAGE.md +15 -0
- package/docs/api/core-version.md +1 -1
- package/docs/api/exports.json +52 -2
- package/docs/api/facade-scene-radiance.md +34 -1
- package/docs/api/facade-simulate-capture.md +66 -2
- package/docs/api/facade-simulate-display-view.md +57 -0
- package/docs/api/facade-subject-framing.md +16 -0
- package/docs/api/output-print-plan.md +8 -1
- package/package.json +3 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import type { EvidenceProvenance } from "../core/evidence-provenance.js";
|
|
2
2
|
import { type GenericEquipmentTier, type GenericEquipmentTierPreset } from "../equipment/generic-tier-assets.js";
|
|
3
|
-
import { type ResolvedWhiteBalanceState } from "../color/white-balance.js";
|
|
3
|
+
import { resolveColorTemperatureWhiteBalance, type ResolvedWhiteBalanceState } from "../color/white-balance.js";
|
|
4
|
+
import { type CaptureWhiteBalanceIntent } from "../capture/simulated-capture.js";
|
|
4
5
|
import { type NativeRawOutput, type NativeRawPlan } from "../capture/native-raw.js";
|
|
5
6
|
import { type NativeEnvironmentRawInput } from "../capture/native-environment-raw.js";
|
|
6
7
|
import { type NativeRawDevelopmentInput, type RawHighlightDiagnostics } from "../capture/native-raw-development.js";
|
|
8
|
+
import type { RawFrameReconstructionPhaseProfile } from "../capture/raw-frame-reconstruction.js";
|
|
7
9
|
import { type SdrToneCurve, type SdrToneCurveParameters, type SdrRenderingResult } from "../output/sdr-rendering.js";
|
|
8
10
|
import type { RawHighlightMode } from "../color/sensor-color-development.js";
|
|
9
11
|
import type { SimulateEnvironmentSensorRawFrameInput } from "../capture/environment-raw-producer.js";
|
|
@@ -12,11 +14,12 @@ import { type LensProjectionModel } from "../camera/lens-projection.js";
|
|
|
12
14
|
import type { RadialDistortionProfile } from "../optics/radial-distortion.js";
|
|
13
15
|
import type { BrownConradyDistortionProfile } from "../optics/brown-conrady-distortion.js";
|
|
14
16
|
import type { IlluminationVignettingProfile } from "../optics/illumination-vignetting.js";
|
|
17
|
+
import type { CalculateSensorPsfIrradianceQuadratureInput } from "../optics/sensor-psf-quadrature.js";
|
|
15
18
|
import type { CalculateEnvironmentSensorPhotoSignalInput } from "../sensor/environment-photo-signal.js";
|
|
16
19
|
import { type DiffractionLimitedPsfProfile } from "../optics/diffraction-psf-profile.js";
|
|
17
20
|
import { type AntiAliasingPointSplitComponent } from "../sensor/optical-stack.js";
|
|
18
|
-
import { type ChartScene } from "./chart-scenes.js";
|
|
19
|
-
import { type ChartSizeOption, type CaptureIlluminant, type CallerRadianceScene, type CallerReflectanceScene } from "./scene-radiance.js";
|
|
21
|
+
import { type ChartScene, type ResolvedChartScene } from "./chart-scenes.js";
|
|
22
|
+
import { type ChartSizeOption, type CaptureIlluminant, type CallerMaterialScene, type CallerRadianceScene, type CallerReflectanceScene, type ResolvedCallerScene, type PlanarSceneRadianceProvider, type ResolvedCaptureIlluminant } from "./scene-radiance.js";
|
|
20
23
|
/** Version of how the facade builds requests. It changes whenever the same input would build a different request. */
|
|
21
24
|
export declare const SIMULATE_CAPTURE_FACADE_VERSION: "1.0.0";
|
|
22
25
|
export type SimulateCaptureExposureMode = "manual" | "aperture-priority" | "shutter-priority" | "program";
|
|
@@ -160,7 +163,7 @@ export interface SimulateCaptureOptions {
|
|
|
160
163
|
compensationStops?: number;
|
|
161
164
|
};
|
|
162
165
|
/** Required: an engine chart (for example `{ preset: "uniform" }`, an 18 % grey card) or a caller reflectance/radiance function. */
|
|
163
|
-
scene: ChartScene | CallerReflectanceScene | CallerRadianceScene;
|
|
166
|
+
scene: ChartScene | CallerReflectanceScene | CallerRadianceScene | CallerMaterialScene;
|
|
164
167
|
/** Distance from the camera to the scene plane in metres. Default 5. */
|
|
165
168
|
subjectDistanceM?: number;
|
|
166
169
|
/**
|
|
@@ -227,7 +230,9 @@ export interface SimulateCaptureOptions {
|
|
|
227
230
|
* the whole render: framing, metering, exposure, noise seeds (absolute native site indices), rolling-shutter row
|
|
228
231
|
* timing, diffraction and demosaic all use the whole frame. Use it to tile a large frame across workers or to
|
|
229
232
|
* render a 100 % crop. `result.image` is the region; `result.region` reports it. Not combinable with
|
|
230
|
-
* `whiteBalance: "auto"`, whose statistics would come from the region only.
|
|
233
|
+
* `whiteBalance: "auto"`, whose statistics would come from the region only. From 2.6 the diffraction convolution
|
|
234
|
+
* covers only the region plus the kernel and filter reach, so a small region of a large frame costs about as much
|
|
235
|
+
* as a capture of the region's size.
|
|
231
236
|
*/
|
|
232
237
|
region?: {
|
|
233
238
|
x: number;
|
|
@@ -451,12 +456,38 @@ export interface SimulateCaptureResult {
|
|
|
451
456
|
};
|
|
452
457
|
assumptions: readonly SimulateCaptureAssumption[];
|
|
453
458
|
}
|
|
459
|
+
export declare const FACADE_EVIDENCE: readonly EvidenceProvenance[];
|
|
454
460
|
/**
|
|
455
461
|
* Default illuminance, lux: an 18 % Lambertian grey card (L = 0.18·E/π) then gets exactly the CIPA DC-004 standard
|
|
456
462
|
* output sensitivity reference exposure H = 10/S at f/8, 1/125 s, ISO 100:
|
|
457
463
|
* E = π·10·N²/(0.65·t·S·0.18) = π·80 000/11.7 ≈ 21 480.98 lux.
|
|
458
464
|
*/
|
|
459
465
|
export declare const SIMULATE_CAPTURE_DEFAULT_ILLUMINANCE_LUX: number;
|
|
466
|
+
interface ParsedAntiAliasing {
|
|
467
|
+
kind: SimulateCaptureSensorAntiAliasing["kind"];
|
|
468
|
+
separationSites?: number;
|
|
469
|
+
axis?: "horizontal" | "vertical";
|
|
470
|
+
components: AntiAliasingPointSplitComponent[];
|
|
471
|
+
}
|
|
472
|
+
/** Midpoint nodes of an N × N rule over a square photosite, as offsets from its centre in sites (+x right, +y down). */
|
|
473
|
+
export declare function apertureNodeOffsets(n: number): {
|
|
474
|
+
x: number;
|
|
475
|
+
y: number;
|
|
476
|
+
}[];
|
|
477
|
+
export declare const CHANNELS: readonly ["red", "green", "blue"];
|
|
478
|
+
type Channel = typeof CHANNELS[number];
|
|
479
|
+
export declare const CAMERA_TO_XYZ: readonly (readonly number[])[];
|
|
480
|
+
/** Synthetic dark current per site at the operating temperature, e⁻/s. */
|
|
481
|
+
export declare const DARK_CURRENT_ELECTRONS_PER_SECOND = 1;
|
|
482
|
+
export declare function fail(message: string): never;
|
|
483
|
+
type ReadoutProfile = GenericEquipmentTierPreset["body"]["readout"][number]["profile"];
|
|
484
|
+
/** Diagnostic clock for the 2.1 stage timings: `performance.now()` where the host has it. Never affects output. */
|
|
485
|
+
export declare function clockMs(): number;
|
|
486
|
+
export declare function allowKeys(value: unknown, name: string, keys: readonly string[]): Record<string, unknown>;
|
|
487
|
+
/** The white balance measured from a perfect white diffuser under the scene illuminant ("scene-illuminant"). */
|
|
488
|
+
export declare function sceneIlluminantWhiteBalance(illuminant: ResolvedCaptureIlluminant): ResolvedWhiteBalanceState;
|
|
489
|
+
/** Bilinear RGGB demosaic as explicit linear neighbourhood kernels (weights sum to one per output channel). */
|
|
490
|
+
export declare function bilinearPhaseProfiles(): RawFrameReconstructionPhaseProfile[];
|
|
460
491
|
/**
|
|
461
492
|
* Simulates one still capture of a planar scene and returns the developed image, the packed RAW, the full
|
|
462
493
|
* underlying request/plan, provenance and every assumption made. See docs/QUICK_START.md.
|
|
@@ -524,5 +555,202 @@ export interface IdealReferenceResult {
|
|
|
524
555
|
/** The capture request's assumptions, then `reference.kind`, `reference.excludes`, `reference.exposure` and `reference.sampling`. */
|
|
525
556
|
assumptions: readonly SimulateCaptureAssumption[];
|
|
526
557
|
}
|
|
558
|
+
/** Default stratified samples per axis of a display view (`view.samplesPerAxis`): 16 × 16 sampled real pixels per display pixel with detail, 4 × 4 where it is uniform. */
|
|
559
|
+
export declare const SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS = 16;
|
|
560
|
+
/** The display size and sampling of {@link simulateDisplayView}. */
|
|
561
|
+
export interface SimulateDisplayViewOptions {
|
|
562
|
+
/** Display pixels, 1 to the capture's output width and height. */
|
|
563
|
+
width: number;
|
|
564
|
+
height: number;
|
|
565
|
+
/** Stratified Monte Carlo samples per axis for each display pixel's expected value (1–32, default 8: 64 real pixels). */
|
|
566
|
+
samplesPerAxis?: number;
|
|
567
|
+
/** "srgb-8" (default) or "srgb-16". */
|
|
568
|
+
format?: "srgb-8" | "srgb-16";
|
|
569
|
+
/** Render only this rectangle of the display view, in display pixels; byte-identical to the same pixels of the whole view. */
|
|
570
|
+
region?: {
|
|
571
|
+
x: number;
|
|
572
|
+
y: number;
|
|
573
|
+
width: number;
|
|
574
|
+
height: number;
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
/** Result of {@link simulateDisplayView}: the full-resolution capture resized for display, estimated; not a capture. */
|
|
578
|
+
export interface DisplayViewResult {
|
|
579
|
+
kind: "display-view";
|
|
580
|
+
/** Human-readable label for a user interface. */
|
|
581
|
+
label: string;
|
|
582
|
+
facadeVersion: typeof SIMULATE_CAPTURE_FACADE_VERSION;
|
|
583
|
+
engineApiVersion: string;
|
|
584
|
+
/** The display view (or its region): interleaved sRGB samples, row by row from the top-left. */
|
|
585
|
+
image: {
|
|
586
|
+
width: number;
|
|
587
|
+
height: number;
|
|
588
|
+
format: "srgb-8" | "srgb-16";
|
|
589
|
+
samples: Uint8Array | Uint16Array;
|
|
590
|
+
};
|
|
591
|
+
/**
|
|
592
|
+
* The view: display size, the capture frame it shows (output pixels), real pixels per display pixel along each axis,
|
|
593
|
+
* the samples per axis and, with `view.region`, the rendered rectangle.
|
|
594
|
+
*/
|
|
595
|
+
view: {
|
|
596
|
+
width: number;
|
|
597
|
+
height: number;
|
|
598
|
+
frameWidth: number;
|
|
599
|
+
frameHeight: number;
|
|
600
|
+
scaleX: number;
|
|
601
|
+
scaleY: number;
|
|
602
|
+
samplesPerAxis: number;
|
|
603
|
+
region?: {
|
|
604
|
+
x: number;
|
|
605
|
+
y: number;
|
|
606
|
+
width: number;
|
|
607
|
+
height: number;
|
|
608
|
+
};
|
|
609
|
+
};
|
|
610
|
+
/** The resolved exposure of the capture this view shows. */
|
|
611
|
+
exposure: {
|
|
612
|
+
mode: SimulateCaptureExposureMode;
|
|
613
|
+
settings: {
|
|
614
|
+
fNumber: number;
|
|
615
|
+
shutterSeconds: number;
|
|
616
|
+
iso: number;
|
|
617
|
+
};
|
|
618
|
+
ev100: number;
|
|
619
|
+
};
|
|
620
|
+
scene: SimulateCaptureResult["scene"];
|
|
621
|
+
/**
|
|
622
|
+
* Work done: real pixels sampled, real-site expectations evaluated, scene queries, display pixels sampled in full (detail) or with
|
|
623
|
+
* a quarter of the samples per axis (uniform), display pixels whose value includes the AHD low-signal correction, and wall-clock
|
|
624
|
+
* milliseconds (diagnostic only; never affects the samples).
|
|
625
|
+
*/
|
|
626
|
+
diagnostics: {
|
|
627
|
+
sampleCount: number;
|
|
628
|
+
siteEvaluations: number;
|
|
629
|
+
sceneLookups: number;
|
|
630
|
+
detailPixelCount: number;
|
|
631
|
+
uniformPixelCount: number;
|
|
632
|
+
ahdCorrectedPixelCount: number;
|
|
633
|
+
timingsMs: {
|
|
634
|
+
view: number;
|
|
635
|
+
total: number;
|
|
636
|
+
};
|
|
637
|
+
};
|
|
638
|
+
provenance: {
|
|
639
|
+
scientificStatus: "approximation";
|
|
640
|
+
cameraPresetId: string;
|
|
641
|
+
cameraPresetVersion: string;
|
|
642
|
+
evidence: readonly EvidenceProvenance[];
|
|
643
|
+
composedContracts: readonly string[];
|
|
644
|
+
limitations: readonly string[];
|
|
645
|
+
};
|
|
646
|
+
/** The capture request's assumptions, then `view.definition`, `view.estimation`, `view.noise`, `view.demosaic` and `view.source`. */
|
|
647
|
+
assumptions: readonly SimulateCaptureAssumption[];
|
|
648
|
+
}
|
|
649
|
+
/** Internal: the resolved request state that the display view's renderer (simulate-display-view.ts) reads. */
|
|
650
|
+
export interface DisplayViewRunContext {
|
|
651
|
+
ahd: boolean;
|
|
652
|
+
antiAliasing: ParsedAntiAliasing | undefined;
|
|
653
|
+
aspect: number;
|
|
654
|
+
assume: (id: string, kind: SimulateCaptureAssumption["kind"], value: unknown, explanation: string) => void;
|
|
655
|
+
assumptions: SimulateCaptureAssumption[];
|
|
656
|
+
axisChannelResponse: (caller: string) => {
|
|
657
|
+
electronsPerRadiance: Record<Channel, Map<number, number>>;
|
|
658
|
+
wavelengths: number[];
|
|
659
|
+
relativePerElectron: Record<Channel, number>;
|
|
660
|
+
};
|
|
661
|
+
boxSamples: number;
|
|
662
|
+
buildProvider: (sceneIlluminant: ResolvedCaptureIlluminant, scale: number) => PlanarSceneRadianceProvider;
|
|
663
|
+
callerScene: ResolvedCallerScene | undefined;
|
|
664
|
+
chart: ResolvedChartScene | undefined;
|
|
665
|
+
chartReport: {
|
|
666
|
+
widthM: number;
|
|
667
|
+
heightM: number;
|
|
668
|
+
surroundReflectance: number;
|
|
669
|
+
frameWidthFraction: number;
|
|
670
|
+
frameHeightFraction: number;
|
|
671
|
+
magnification: number;
|
|
672
|
+
imageWidthMm: number;
|
|
673
|
+
imageHeightMm: number;
|
|
674
|
+
imageWidthPixels: number;
|
|
675
|
+
imageHeightPixels: number;
|
|
676
|
+
} | undefined;
|
|
677
|
+
chartSize: {
|
|
678
|
+
widthM: number;
|
|
679
|
+
heightM: number;
|
|
680
|
+
surroundReflectance: number;
|
|
681
|
+
defaulted: ("heightM" | "surroundReflectance")[];
|
|
682
|
+
} | undefined;
|
|
683
|
+
diffraction: DiffractionLimitedPsfProfile | undefined;
|
|
684
|
+
diffractionPsf: {
|
|
685
|
+
kind: "sampled-local";
|
|
686
|
+
configuration: Pick<CalculateSensorPsfIrradianceQuadratureInput, "psf" | "psfWavelengthBasis" | "spatialModel">;
|
|
687
|
+
} | undefined;
|
|
688
|
+
fNumber: number;
|
|
689
|
+
focalLengthMm: number;
|
|
690
|
+
focusDistanceM: number;
|
|
691
|
+
height: number;
|
|
692
|
+
highlightMode: RawHighlightMode;
|
|
693
|
+
illuminanceLux: number;
|
|
694
|
+
illuminant: ResolvedCaptureIlluminant;
|
|
695
|
+
illuminantOption: CaptureIlluminant;
|
|
696
|
+
imagingArea: {
|
|
697
|
+
widthMm: number;
|
|
698
|
+
heightMm: number;
|
|
699
|
+
};
|
|
700
|
+
intentOf: (state: ResolvedWhiteBalanceState | null) => CaptureWhiteBalanceIntent | null;
|
|
701
|
+
metresPerFrameUnit: number;
|
|
702
|
+
noiseSeed: number;
|
|
703
|
+
outputOptions: Record<string, unknown>;
|
|
704
|
+
parsedView: ParsedDisplayView | undefined;
|
|
705
|
+
pitch: number;
|
|
706
|
+
preset: GenericEquipmentTierPreset;
|
|
707
|
+
provider: PlanarSceneRadianceProvider;
|
|
708
|
+
regimeByChannel: Record<"red" | "green" | "blue", {
|
|
709
|
+
readoutProfile: ReadoutProfile;
|
|
710
|
+
regimeId: string;
|
|
711
|
+
}>;
|
|
712
|
+
resolvedIso: number;
|
|
713
|
+
sceneOption: unknown;
|
|
714
|
+
scenePreset: "composite" | "uniform" | "patches" | "slanted-edge" | "siemens-star" | "step-wedge" | "grey-ramp" | "checkerboard" | "point-grid" | "dead-leaves" | "bar-groups" | "hyperbolic-wedge" | "zone-plate" | "slanted-squares" | "line-grid" | "fiducial" | "reflectance-function" | "radiance-function" | "material-function";
|
|
715
|
+
shutterSeconds: number;
|
|
716
|
+
started: number;
|
|
717
|
+
subjectDistanceM: number;
|
|
718
|
+
toneCurve: SdrToneCurve;
|
|
719
|
+
whiteBalanceOption: SimulateCaptureWhiteBalance;
|
|
720
|
+
whiteBalanceState: ResolvedWhiteBalanceState | null;
|
|
721
|
+
width: number;
|
|
722
|
+
checkAbort: () => void;
|
|
723
|
+
margin: number;
|
|
724
|
+
mode: SimulateCaptureExposureMode;
|
|
725
|
+
nativeHeight: number;
|
|
726
|
+
nativeWidth: number;
|
|
727
|
+
sourceSha256: string;
|
|
728
|
+
yieldControl: () => Promise<void>;
|
|
729
|
+
/** The Kelvin white balance request, which the view resolves with the portable functions. */
|
|
730
|
+
kelvinWhiteBalanceInput: Parameters<typeof resolveColorTemperatureWhiteBalance>[0] | undefined;
|
|
731
|
+
}
|
|
732
|
+
export interface ParsedDisplayView {
|
|
733
|
+
width: number;
|
|
734
|
+
height: number;
|
|
735
|
+
samplesPerAxis: number;
|
|
736
|
+
bitDepth: 8 | 16;
|
|
737
|
+
format: "srgb-8" | "srgb-16";
|
|
738
|
+
region?: {
|
|
739
|
+
x: number;
|
|
740
|
+
y: number;
|
|
741
|
+
width: number;
|
|
742
|
+
height: number;
|
|
743
|
+
};
|
|
744
|
+
}
|
|
745
|
+
/** Internal: validates a display view against the capture's output size and fills its defaults. */
|
|
746
|
+
export declare function parseDisplayViewOption(value: unknown, frameWidth: number, frameHeight: number): ParsedDisplayView;
|
|
747
|
+
/** Internal: passed as the view of a "display-view" run to resolve the request without a view (`prepareDisplayView`, #387). */
|
|
748
|
+
export declare const DISPLAY_VIEW_WITHOUT_VIEW: unique symbol;
|
|
749
|
+
/**
|
|
750
|
+
* Internal: the request path shared by {@link simulateCapture}, {@link simulateIdealReference} and the display view, whose renderer is
|
|
751
|
+
* passed in; for a display view the renderer's value is returned (a result, or a prepared view with DISPLAY_VIEW_WITHOUT_VIEW).
|
|
752
|
+
*/
|
|
753
|
+
export declare function runCapture(options: SimulateCaptureOptions, run: "capture" | "ideal-reference" | "display-view", viewOption?: unknown, renderView?: (context: DisplayViewRunContext) => Promise<unknown>): Promise<unknown>;
|
|
527
754
|
/** Chart preset ids accepted by `simulateCapture({ scene: { preset } })`. */
|
|
528
755
|
export declare const SIMULATE_CAPTURE_CHART_PRESETS: readonly ("composite" | "uniform" | "patches" | "slanted-edge" | "siemens-star" | "step-wedge" | "grey-ramp" | "checkerboard" | "point-grid" | "dead-leaves" | "bar-groups" | "hyperbolic-wedge" | "zone-plate" | "slanted-squares" | "line-grid" | "fiducial")[];
|
|
756
|
+
export {};
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
* without it every encoded file is byte-identical to 2.3.
|
|
29
29
|
* @see docs/QUICK_START.md
|
|
30
30
|
*/
|
|
31
|
+
import { markInternalSceneRadianceEvaluator } from "../core/internal-radiance-evaluator.js";
|
|
31
32
|
import { InvalidConfigurationError } from "../core/configuration-error.js";
|
|
32
33
|
import { requireInputObject } from "../core/record-validation.js";
|
|
33
34
|
import { ENGINE_API_VERSION } from "../core/version.js";
|
|
@@ -92,7 +93,7 @@ export const SIMULATE_CAPTURE_COST_OPTIONS = Object.freeze(["lens.pupilSampling"
|
|
|
92
93
|
"lens.diffraction", "sensor.antiAliasing", "sensor.pixelAperture.box", "sensor.temporalSampleCount", "motion.rotation", "motion.shutter.rolling", "output.borderHandling.mirror", "output.region",
|
|
93
94
|
"output.whiteBalance.auto", "output.demosaic.ahd", "output.highlightMode.reconstruct", "output.colorEncoding.wide-gamut", "output.gamutMapping.perceptual",
|
|
94
95
|
"output.format.jpeg", "output.format.png", "output.format.dng"]);
|
|
95
|
-
const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
96
|
+
export const FACADE_EVIDENCE = Object.freeze([Object.freeze({ sourceOrigin: "photivra",
|
|
96
97
|
sourceReference: "photivra:simulate-capture-facade", reuseStatus: "photivra-owned" })]);
|
|
97
98
|
const DEFAULTS = Object.freeze({ camera: "professional", fNumber: 8, shutterSeconds: 1 / 125, iso: 100, compensationStops: 0,
|
|
98
99
|
subjectDistanceM: 5, width: 64, height: 64, format: "srgb-8", toneCurve: "identity", highlightMode: "clip",
|
|
@@ -181,7 +182,7 @@ function parseAntiAliasing(value, pitch) {
|
|
|
181
182
|
return fail("sensor.antiAliasing.kind must be four-spot, two-spot or point-splitting.");
|
|
182
183
|
}
|
|
183
184
|
/** Midpoint nodes of an N × N rule over a square photosite, as offsets from its centre in sites (+x right, +y down). */
|
|
184
|
-
function apertureNodeOffsets(n) {
|
|
185
|
+
export function apertureNodeOffsets(n) {
|
|
185
186
|
const out = [];
|
|
186
187
|
for (let j = 0; j < n; j++)
|
|
187
188
|
for (let i = 0; i < n; i++)
|
|
@@ -197,7 +198,7 @@ const IDS = Object.freeze({ frame: "facade-frame", mode: "native-still", cfa: "p
|
|
|
197
198
|
capture: "facade-capture", spectral: "facade-colorimetric-eqe", sampling: "facade-sampling", stack: "facade-no-aa", stackAa: "facade-aa",
|
|
198
199
|
stationarity: "facade-stationarity", operatingState: "facade-operating-state", dark: "facade-dark", completeness: "facade-completeness",
|
|
199
200
|
chargeSampling: "facade-charge-sampling", capacity: "facade-capacity" });
|
|
200
|
-
const CHANNELS = ["red", "green", "blue"];
|
|
201
|
+
export const CHANNELS = ["red", "green", "blue"];
|
|
201
202
|
/**
|
|
202
203
|
* Synthetic colorimetric sensor: channel EQE(λ) = a·cmf(λ)·(555 nm/λ), with cmf = x̄, ȳ, z̄. The λ factor turns
|
|
203
204
|
* the energy-weighted CMF (colour-matching function) into a photon-counting response. So channel counts are
|
|
@@ -206,12 +207,12 @@ const CHANNELS = ["red", "green", "blue"];
|
|
|
206
207
|
*/
|
|
207
208
|
const EQE_SCALE = Object.freeze({ red: 0.5, green: 0.5, blue: 0.25 });
|
|
208
209
|
const CMF_KEY = Object.freeze({ red: "x", green: "y", blue: "z" });
|
|
209
|
-
const CAMERA_TO_XYZ = Object.freeze([[EQE_SCALE.green / EQE_SCALE.red, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.green / EQE_SCALE.blue]].map((r) => Object.freeze(r)));
|
|
210
|
+
export const CAMERA_TO_XYZ = Object.freeze([[EQE_SCALE.green / EQE_SCALE.red, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.green / EQE_SCALE.blue]].map((r) => Object.freeze(r)));
|
|
210
211
|
const XYZ_TO_CAMERA = [[EQE_SCALE.red / EQE_SCALE.green, 0, 0], [0, 1, 0], [0, 0, EQE_SCALE.blue / EQE_SCALE.green]];
|
|
211
212
|
/** Synthetic dark current per site at the operating temperature, e⁻/s. */
|
|
212
|
-
const DARK_CURRENT_ELECTRONS_PER_SECOND = 1;
|
|
213
|
+
export const DARK_CURRENT_ELECTRONS_PER_SECOND = 1;
|
|
213
214
|
const OPERATING_TEMPERATURE_C = 20;
|
|
214
|
-
function fail(message) { throw new InvalidConfigurationError(message); }
|
|
215
|
+
export function fail(message) { throw new InvalidConfigurationError(message); }
|
|
215
216
|
/** Where `output.metadata` goes in each file format (reported in the `output.metadata` assumption). */
|
|
216
217
|
const TEXT_METADATA_LAYOUT = Object.freeze({
|
|
217
218
|
jpeg: "XMP packet in APP1 (dc:description, xmp:CreatorTool, dc:rights, exif:UserComment), plus a COM segment holding the comment",
|
|
@@ -230,7 +231,7 @@ function isoAreaScaledReadout(profile, areaScale) {
|
|
|
230
231
|
preAdcSaturationElectronEquivalent: { ...regime.preAdcSaturationElectronEquivalent, value: regime.preAdcSaturationElectronEquivalent.value * areaScale } })) });
|
|
231
232
|
}
|
|
232
233
|
/** Diagnostic clock for the 2.1 stage timings: `performance.now()` where the host has it. Never affects output. */
|
|
233
|
-
function clockMs() { return globalThis.performance?.now() ?? Date.now(); }
|
|
234
|
+
export function clockMs() { return globalThis.performance?.now() ?? Date.now(); }
|
|
234
235
|
function positive(value, name) {
|
|
235
236
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
|
|
236
237
|
fail(name + " must be a positive finite number.");
|
|
@@ -241,7 +242,7 @@ function integerIn(value, name, minimum, maximum) {
|
|
|
241
242
|
fail(name + " must be an integer from " + minimum + " to " + maximum + ".");
|
|
242
243
|
return value;
|
|
243
244
|
}
|
|
244
|
-
function allowKeys(value, name, keys) {
|
|
245
|
+
export function allowKeys(value, name, keys) {
|
|
245
246
|
if (value === undefined)
|
|
246
247
|
return {};
|
|
247
248
|
requireInputObject(value, name);
|
|
@@ -284,6 +285,12 @@ function luminanceOf(radiance) {
|
|
|
284
285
|
}
|
|
285
286
|
return PHOTOPIC_MAXIMUM_LUMINOUS_EFFICACY_LM_PER_W * sum;
|
|
286
287
|
}
|
|
288
|
+
/** The white balance measured from a perfect white diffuser under the scene illuminant ("scene-illuminant"). */
|
|
289
|
+
export function sceneIlluminantWhiteBalance(illuminant) {
|
|
290
|
+
const xyz = illuminantXyz(illuminant), camera = XYZ_TO_CAMERA.map((row) => row[0] * xyz[0] + row[1] * xyz[1] + row[2] * xyz[2]);
|
|
291
|
+
return resolveCustomWhiteBalance({ stateId: "facade-wb-scene-illuminant", sampleSet: { imageStateId: "facade-white-diffuser",
|
|
292
|
+
inputDomain: "relative-pre-wb-camera-linear-rgb", samples: [{ sampleId: "white", red: camera[0], green: camera[1], blue: camera[2], weight: 1, clipped: false }] } });
|
|
293
|
+
}
|
|
287
294
|
function illuminantXyz(illuminant) {
|
|
288
295
|
let x = 0, y = 0, z = 0;
|
|
289
296
|
for (let nm = FACADE_SPECTRAL_RANGE_NANOMETERS.minimum; nm <= FACADE_SPECTRAL_RANGE_NANOMETERS.maximum; nm++) {
|
|
@@ -295,7 +302,7 @@ function illuminantXyz(illuminant) {
|
|
|
295
302
|
return [x / y, 1, z / y];
|
|
296
303
|
}
|
|
297
304
|
/** Bilinear RGGB demosaic as explicit linear neighbourhood kernels (weights sum to one per output channel). */
|
|
298
|
-
function bilinearPhaseProfiles() {
|
|
305
|
+
export function bilinearPhaseProfiles() {
|
|
299
306
|
const cross = [[1, 0], [-1, 0], [0, 1], [0, -1]], diagonal = [[1, 1], [1, -1], [-1, 1], [-1, -1]], horizontal = [[1, 0], [-1, 0]], vertical = [[0, 1], [0, -1]];
|
|
300
307
|
const average = (offsets, channel) => offsets.map(([x, y]) => ({ offsetX: x, offsetY: y, sourceChannelId: channel, weight: 1 / offsets.length }));
|
|
301
308
|
const self = (channel) => [{ offsetX: 0, offsetY: 0, sourceChannelId: channel, weight: 1 }];
|
|
@@ -470,7 +477,7 @@ function encodeSdrFromLinear(rgb, encoding, bitDepth, mapping) {
|
|
|
470
477
|
* underlying request/plan, provenance and every assumption made. See docs/QUICK_START.md.
|
|
471
478
|
*/
|
|
472
479
|
export async function simulateCapture(options) {
|
|
473
|
-
return await runCapture(options,
|
|
480
|
+
return await runCapture(options, "capture");
|
|
474
481
|
}
|
|
475
482
|
/**
|
|
476
483
|
* The ideal reference of a capture request (2.3): the same scene, framing, chart size, output size, region and pixel
|
|
@@ -482,10 +489,36 @@ export async function simulateCapture(options) {
|
|
|
482
489
|
* @see docs/QUICK_START.md, "Ideal reference".
|
|
483
490
|
*/
|
|
484
491
|
export async function simulateIdealReference(options) {
|
|
485
|
-
return await runCapture(options,
|
|
492
|
+
return await runCapture(options, "ideal-reference");
|
|
493
|
+
}
|
|
494
|
+
/** Default stratified samples per axis of a display view (`view.samplesPerAxis`): 16 × 16 sampled real pixels per display pixel with detail, 4 × 4 where it is uniform. */
|
|
495
|
+
export const SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS = 16;
|
|
496
|
+
/** Internal: validates a display view against the capture's output size and fills its defaults. */
|
|
497
|
+
export function parseDisplayViewOption(value, frameWidth, frameHeight) {
|
|
498
|
+
if (value === undefined)
|
|
499
|
+
fail("simulateDisplayView needs a view, for example { width: 384, height: 256 }.");
|
|
500
|
+
const v = allowKeys(value, "view", ["width", "height", "samplesPerAxis", "format", "region"]);
|
|
501
|
+
const width = integerIn(v.width, "view.width", 1, frameWidth), height = integerIn(v.height, "view.height", 1, frameHeight);
|
|
502
|
+
const samplesPerAxis = v.samplesPerAxis === undefined ? SIMULATE_DISPLAY_VIEW_DEFAULT_SAMPLES_PER_AXIS : integerIn(v.samplesPerAxis, "view.samplesPerAxis", 1, 32);
|
|
503
|
+
const format = v.format === undefined ? "srgb-8" : v.format;
|
|
504
|
+
if (format !== "srgb-8" && format !== "srgb-16")
|
|
505
|
+
fail("view.format must be srgb-8 or srgb-16.");
|
|
506
|
+
let region;
|
|
507
|
+
if (v.region !== undefined) {
|
|
508
|
+
const r = allowKeys(v.region, "view.region", ["x", "y", "width", "height"]);
|
|
509
|
+
const x = integerIn(r.x, "view.region.x", 0, width - 1), y = integerIn(r.y, "view.region.y", 0, height - 1);
|
|
510
|
+
region = { x, y, width: integerIn(r.width, "view.region.width", 1, width - x), height: integerIn(r.height, "view.region.height", 1, height - y) };
|
|
511
|
+
}
|
|
512
|
+
return { width, height, samplesPerAxis, bitDepth: format === "srgb-8" ? 8 : 16, format: format, ...(region ? { region } : {}) };
|
|
486
513
|
}
|
|
487
|
-
|
|
488
|
-
|
|
514
|
+
/** Internal: passed as the view of a "display-view" run to resolve the request without a view (`prepareDisplayView`, #387). */
|
|
515
|
+
export const DISPLAY_VIEW_WITHOUT_VIEW = Symbol("display view without view");
|
|
516
|
+
/**
|
|
517
|
+
* Internal: the request path shared by {@link simulateCapture}, {@link simulateIdealReference} and the display view, whose renderer is
|
|
518
|
+
* passed in; for a display view the renderer's value is returned (a result, or a prepared view with DISPLAY_VIEW_WITHOUT_VIEW).
|
|
519
|
+
*/
|
|
520
|
+
export async function runCapture(options, run, viewOption, renderView) {
|
|
521
|
+
const started = Date.now(), idealReference = run === "ideal-reference", displayView = run === "display-view";
|
|
489
522
|
requireInputObject(options, "simulateCapture options");
|
|
490
523
|
const top = allowKeys(options, "simulateCapture options", ["camera", "lens", "exposure", "scene", "subjectDistanceM", "chartSize", "illuminant",
|
|
491
524
|
"illuminanceLux", "output", "sensor", "motion", "signal", "onProgress"]);
|
|
@@ -714,6 +747,29 @@ async function runCapture(options, idealReference) {
|
|
|
714
747
|
}
|
|
715
748
|
}
|
|
716
749
|
const boxAperture = sensorOptions.pixelAperture !== undefined && sensorOptions.pixelAperture !== "point";
|
|
750
|
+
// Display view: its own parameters, and the capture options it reproduces, checked before any scene work.
|
|
751
|
+
const parsedView = displayView && viewOption !== DISPLAY_VIEW_WITHOUT_VIEW ? parseDisplayViewOption(viewOption, width, height) : undefined;
|
|
752
|
+
if (displayView) {
|
|
753
|
+
const unsupported = (option, reason) => fail("simulateDisplayView does not support " + option + ": " + reason);
|
|
754
|
+
if (pupil)
|
|
755
|
+
unsupported("lens.pupilSampling", "defocus moves each query off the site lattice the expected-signal tables use.");
|
|
756
|
+
if (lensFieldMapping)
|
|
757
|
+
unsupported("lens.projection or lens.distortion", "the expected signal assumes the ideal rectilinear, distortion-free mapping of the site lattice.");
|
|
758
|
+
if (vignetting)
|
|
759
|
+
unsupported("lens.vignetting", "the expected signal assumes the same radiance-to-irradiance factor at every site.");
|
|
760
|
+
if (lensPsf)
|
|
761
|
+
unsupported("lens.psf", "only the engine-built diffraction kernels (lens.diffraction) are tabulated.");
|
|
762
|
+
if (rotating)
|
|
763
|
+
unsupported("motion.angularVelocityRadPerSec", "camera rotation moves the queries off the site lattice.");
|
|
764
|
+
if (outputOptions.region !== undefined)
|
|
765
|
+
unsupported("output.region", "pass the rectangle as view.region, in display pixels.");
|
|
766
|
+
if (outputOptions.whiteBalance === "auto")
|
|
767
|
+
unsupported("output.whiteBalance \"auto\"", "the capture measures it from its noisy full-resolution RAW; use \"scene-illuminant\" or a fixed white balance.");
|
|
768
|
+
if (outputOptions.toneCurve !== undefined && outputOptions.toneCurve !== "identity" && outputOptions.toneCurve !== "positive-reinhard-per-channel")
|
|
769
|
+
unsupported("output.toneCurve " + String(outputOptions.toneCurve), "luminance tone curves couple the channels; the view supports identity and positive-reinhard-per-channel.");
|
|
770
|
+
if (outputOptions.highlightMode === "reconstruct")
|
|
771
|
+
unsupported("output.highlightMode \"reconstruct\"", "the view supports the per-channel highlight clip.");
|
|
772
|
+
}
|
|
717
773
|
const colorSamplingProfile = { schemaVersion: "0.1.0", profileId: IDS.cfa, evidence: FACADE_EVIDENCE, coordinateSystem: "native-sensor-color-sampling-site-index",
|
|
718
774
|
layout: { kind: "periodic-mosaic", repeatWidthSites: 2, repeatHeightSites: 2, siteChannelIds: ["red", "green", "green", "blue"], anchor: "native-sensor-top-left-site" } };
|
|
719
775
|
// Demosaic (2.0): built-in Bayer kernels and border handling. Omitted, the 1.0 bilinear kernels and a
|
|
@@ -752,13 +808,17 @@ async function runCapture(options, idealReference) {
|
|
|
752
808
|
if (region && regionNative)
|
|
753
809
|
assume("output.region", "model", { ...region, frameWidth: width, frameHeight: height, nativeRect: { ...regionNative } }, "Only this rectangle of the " + width + " × " + height + " frame is rendered. Framing, metering, exposure, diffraction kernels and every site's physics, noise seeds and exposure window (rolling-shutter row timing) are those of the whole frame (absolute native indices), " +
|
|
754
810
|
"and the native rectangle includes the demosaic reach on each side (" + (margin > 0 ? margin : "mirror-border") + "), so the pixels are byte-identical to the same pixels of the whole render. " +
|
|
755
|
-
|
|
811
|
+
// 2.6: with lens.diffraction the convolution is limited to the region; the sentence of 2.3–2.5 is kept verbatim otherwise, so
|
|
812
|
+
// region results without diffraction stay byte-identical to 2.5.0.
|
|
813
|
+
(diffractionRadiusOption !== undefined ? "With lens.diffraction the object-space convolution (when it applies) covers only the lattice the region's native sites query (plus the kernel radius), in bands of at most 32 native rows; its values are the whole render's." :
|
|
814
|
+
"With lens.diffraction the object-space convolution runs over whole-width bands of 32 native rows that the region touches."));
|
|
756
815
|
if (raisedPixels !== undefined) {
|
|
757
816
|
if (nativeWidth > NATIVE_RAW_LIMITS.maximumDimension || nativeHeight > NATIVE_RAW_LIMITS.maximumDimension || nativeWidth * nativeHeight > raisedPixels) {
|
|
758
817
|
fail("output.maximumPixels (" + raisedPixels + ") must cover the native sensor raster of " + nativeWidth + " × " + nativeHeight +
|
|
759
818
|
" sites (the output plus the demosaic margin), at most " + NATIVE_RAW_LIMITS.maximumDimension + " per side.");
|
|
760
819
|
}
|
|
761
|
-
|
|
820
|
+
// The display view allocates no full-resolution raster, so the AHD working limit does not apply to it.
|
|
821
|
+
if (ahd && !displayView && (regionNative ? regionNative.width * regionNative.height : nativeWidth * nativeHeight) > PROCESSING_MAX_PIXELS)
|
|
762
822
|
fail("output.demosaic \"ahd\" admits at most " + PROCESSING_MAX_PIXELS + " native sites.");
|
|
763
823
|
}
|
|
764
824
|
const imagingArea = { widthMm: nativeWidth * pitch / 1000, heightMm: nativeHeight * pitch / 1000 };
|
|
@@ -839,18 +899,29 @@ async function runCapture(options, idealReference) {
|
|
|
839
899
|
illuminantRecord?.kind === "daylight" ? { basisDatasetId: illuminantRecord.basis?.datasetId, spectrum: spectrumDigestForm(illuminant.spectrum, illuminant.shape) } : undefined;
|
|
840
900
|
const materialInputs = chart ? chartMaterialInputs(chart) : [];
|
|
841
901
|
const sceneSpectra = chart ? chart.materials.flatMap((m, index) => m.kind !== "spectrum" ? [] : [{ material: index, spectrum: spectrumDigestForm(materialInputs[index], m.spectrum) }]) : [];
|
|
902
|
+
// 2.7: a material scene's materials enter the digest as data (neutral reflectances, and caller spectra as above).
|
|
903
|
+
const sceneMaterials = callerScene?.kind === "material-function" ? callerScene.materials.map((m, index) => (m.kind === "neutral" ? m.reflectance :
|
|
904
|
+
spectrumDigestForm(callerScene.materialInputs[index], m.spectrum))) : undefined;
|
|
842
905
|
const sceneDescription = { facadeVersion: SIMULATE_CAPTURE_FACADE_VERSION, scene: chart ? { preset: chart.preset, parameters: chart.parameters } :
|
|
843
906
|
{ kind: callerScene.kind, sceneId: callerScene.sceneId }, illuminant: illuminant.id, illuminanceLux, subjectDistanceM, framedWidthM: 2 * metresPerFrameUnit, aspect,
|
|
844
907
|
...(chartSize ? { chartSize: { widthM: chartSize.widthM, heightM: chartSize.heightM, surroundReflectance: chartSize.surroundReflectance } } : {}),
|
|
845
|
-
...(illuminantSpectrum === undefined ? {} : { illuminantSpectrum }), ...(sceneSpectra.length === 0 ? {} : { sceneSpectra }) };
|
|
846
|
-
if (callerScene)
|
|
908
|
+
...(illuminantSpectrum === undefined ? {} : { illuminantSpectrum }), ...(sceneSpectra.length === 0 ? {} : { sceneSpectra }), ...(sceneMaterials ? { sceneMaterials } : {}) };
|
|
909
|
+
if (callerScene?.kind === "material-function")
|
|
910
|
+
assume("scene.digest", "model", "caller-function-not-hashed", "The source digest covers the caller's sceneId, its materials and the framing, not the body of the caller's materialAt function.");
|
|
911
|
+
else if (callerScene)
|
|
847
912
|
assume("scene.digest", "model", "caller-function-not-hashed", "The source digest covers the caller's sceneId and the framing, not the body of the caller's evaluate function.");
|
|
848
913
|
const sourceSha256 = await sha256Hex(JSON.stringify(sceneDescription, (_key, value) => (typeof value === "object" && value !== null && "evaluate" in value ? "caller-spectrum" : value)));
|
|
849
914
|
checkAbort();
|
|
850
|
-
const
|
|
851
|
-
illuminant, irradianceScale, geometry: { distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
915
|
+
const buildProvider = (sceneIlluminant, scale) => buildPlanarSceneRadianceProvider({ scene: chart ?? callerScene,
|
|
916
|
+
sceneId: IDS.scene, providerProfileId: IDS.provider, illuminant: sceneIlluminant, irradianceScale: scale, geometry: { distanceM: subjectDistanceM, metresPerFrameUnit,
|
|
852
917
|
...(chartSize ? { chart: { widthM: chartSize.widthM, heightM: chartSize.heightM, surroundReflectance: chartSize.surroundReflectance } } : {}) }, evidence: FACADE_EVIDENCE,
|
|
853
|
-
limitations: ["Planar Lambertian scene under uniform illumination; no shadows, inter-reflection, gloss or fluorescence."] });
|
|
918
|
+
limitations: Object.freeze(["Planar Lambertian scene under uniform illumination; no shadows, inter-reflection, gloss or fluorescence."]) });
|
|
919
|
+
const provider = buildProvider(illuminant, irradianceScale);
|
|
920
|
+
// 2.6: a chart preset's evaluator is engine-built end to end (its results: the schema's exact keys, constant fields,
|
|
921
|
+
// the request's identifiers, deep-frozen FACADE_EVIDENCE, UNCERTAINTY and limitations, and an engine-computed
|
|
922
|
+
// radiance), so the native task may skip re-parsing the fixed parts. Caller scenes keep the full validation.
|
|
923
|
+
if (chart)
|
|
924
|
+
markInternalSceneRadianceEvaluator(provider.evaluateRadiance);
|
|
854
925
|
assume("model.scene-radiometry", "model", "lambertian-plane", "Radiance = reflectance × irradiance / π on one plane; illuminance from K_m = 683 lm/W and the engine's analytic CIE 1931 ȳ fit.");
|
|
855
926
|
// Metering (CIPA DC-004 standard output sensitivity and camera equation) and exposure-mode resolution
|
|
856
927
|
const exposureOptions = allowKeys(top.exposure, "exposure", ["mode", "iso", "shutterSeconds", "compensationStops"]);
|
|
@@ -1020,7 +1091,7 @@ async function runCapture(options, idealReference) {
|
|
|
1020
1091
|
else
|
|
1021
1092
|
assume("lens.projection", "model", projection, "Ideal " + projection + " projection through the environment lens field mapping. At finite focus its scale is the thin-lens image distance (an approximation); the chart is still sized with the rectilinear thin-lens scale, so its edges move with the projection.");
|
|
1022
1093
|
// White balance
|
|
1023
|
-
let whiteBalanceState = null, needsAuto = false;
|
|
1094
|
+
let whiteBalanceState = null, needsAuto = false, kelvinWhiteBalanceInput;
|
|
1024
1095
|
if (whiteBalanceOption === "none")
|
|
1025
1096
|
whiteBalanceState = null;
|
|
1026
1097
|
else if (whiteBalanceOption === "auto") {
|
|
@@ -1028,17 +1099,16 @@ async function runCapture(options, idealReference) {
|
|
|
1028
1099
|
fail("output.region cannot be combined with output.whiteBalance \"auto\": grey-world statistics of a region differ from the whole frame's.");
|
|
1029
1100
|
needsAuto = true;
|
|
1030
1101
|
}
|
|
1031
|
-
else if (whiteBalanceOption === "scene-illuminant")
|
|
1032
|
-
|
|
1033
|
-
whiteBalanceState = resolveCustomWhiteBalance({ stateId: "facade-wb-scene-illuminant", sampleSet: { imageStateId: "facade-white-diffuser",
|
|
1034
|
-
inputDomain: "relative-pre-wb-camera-linear-rgb", samples: [{ sampleId: "white", red: camera[0], green: camera[1], blue: camera[2], weight: 1, clipped: false }] } });
|
|
1035
|
-
}
|
|
1102
|
+
else if (whiteBalanceOption === "scene-illuminant")
|
|
1103
|
+
whiteBalanceState = sceneIlluminantWhiteBalance(illuminant);
|
|
1036
1104
|
else if (typeof whiteBalanceOption === "object" && whiteBalanceOption !== null && "kelvin" in whiteBalanceOption) {
|
|
1037
1105
|
const wb = allowKeys(whiteBalanceOption, "output.whiteBalance", ["kelvin", "tint"]);
|
|
1038
1106
|
const intent = createColorTemperatureWhiteBalanceIntent({ intentId: "facade-wb-cct", colorTemperatureKelvin: positive(wb.kelvin, "output.whiteBalance.kelvin"),
|
|
1039
1107
|
tint: wb.tint === undefined ? 0 : wb.tint });
|
|
1040
|
-
|
|
1041
|
-
camera: { profileId: IDS.spectral, profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, xyzToCameraRgb: XYZ_TO_CAMERA.map((r) => [...r]) } }
|
|
1108
|
+
const kelvinInput = { stateId: "facade-wb-cct", intent, whiteLocus: "planckian", tintInterpretation: "duv-cie-1960-uv",
|
|
1109
|
+
camera: { profileId: IDS.spectral, profileVersion: SIMULATE_CAPTURE_FACADE_VERSION, xyzToCameraRgb: XYZ_TO_CAMERA.map((r) => [...r]) } };
|
|
1110
|
+
whiteBalanceState = resolveColorTemperatureWhiteBalance(kelvinInput);
|
|
1111
|
+
kelvinWhiteBalanceInput = kelvinInput;
|
|
1042
1112
|
}
|
|
1043
1113
|
else if (typeof whiteBalanceOption === "object" && whiteBalanceOption !== null && "channelGains" in whiteBalanceOption) {
|
|
1044
1114
|
allowKeys(whiteBalanceOption, "output.whiteBalance", ["channelGains"]);
|
|
@@ -1236,6 +1306,8 @@ async function runCapture(options, idealReference) {
|
|
|
1236
1306
|
const rawProvider = diffraction && diffractionObjectSpace ? createDiffractedPlanarProvider({ base: provider, psf: diffractionPsf.configuration.psf,
|
|
1237
1307
|
wavelengthsNm: diffraction.profile.axes.wavelengthNm, kernelRadius: diffraction.kernelRadiusSamples, nativeWidth, nativeHeight, outputWidth: width,
|
|
1238
1308
|
sitePitchMicrometers: pitch, distanceM: subjectDistanceM, metresPerFrameUnit, onConvolution: (ms) => { diffractionMs += ms; },
|
|
1309
|
+
// Region (2.6): convolve only the lattice the region's native sites query; the values are the whole render's.
|
|
1310
|
+
...(regionNative ? { window: regionNative } : {}),
|
|
1239
1311
|
// The environment queries each site at each aperture node minus each split offset (sites, +x right, +y down).
|
|
1240
1312
|
...(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;
|
|
1241
1313
|
if (antiAliasing && perSite > 100_000)
|
|
@@ -1245,13 +1317,9 @@ async function runCapture(options, idealReference) {
|
|
|
1245
1317
|
const tileWidth = Math.max(1, Math.min(256, Math.floor(100_000 / perSite)));
|
|
1246
1318
|
const environmentRaw = { raw: buildRawInput(intentOf(whiteBalanceState)), sceneBinding,
|
|
1247
1319
|
maximumProviderEvaluations: Math.min(nativeRawMaximumProviderEvaluations(raisedPixels === undefined ? {} : { maximumPixels: raisedPixels }), Math.max(nativeWidth * nativeHeight, evaluations)) };
|
|
1248
|
-
//
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
if (format !== "srgb-8" && format !== "srgb-16" && format !== "png")
|
|
1252
|
-
fail("simulateIdealReference supports output.format srgb-8, srgb-16 and png: a reference has no RAW (dng) and should not carry JPEG compression artefacts.");
|
|
1253
|
-
if (needsAuto)
|
|
1254
|
-
fail("simulateIdealReference cannot reproduce output.whiteBalance \"auto\": the capture measures it from its noisy RAW. Use \"scene-illuminant\" or a fixed white balance.");
|
|
1320
|
+
// The facade's environment chain at a site next to the optical axis, shared by the ideal reference (2.3) and the display
|
|
1321
|
+
// view: expected electrons per unit spectral radiance per channel and node, and the normalized RAW signal per electron.
|
|
1322
|
+
function axisChannelResponse(caller) {
|
|
1255
1323
|
// 1. Electrons per unit spectral radiance at each spectral node, per channel: the facade's own environment chain at a
|
|
1256
1324
|
// site next to the optical axis, with the ideal optics (no PSF, pupil, distortion, vignetting or motion). The signal
|
|
1257
1325
|
// is linear in the radiance node values and, for this planar scene, the same at every site (paraxial π/(4N_w²)).
|
|
@@ -1260,7 +1328,7 @@ async function runCapture(options, idealReference) {
|
|
|
1260
1328
|
const electronsPerRadiance = {};
|
|
1261
1329
|
for (const channel of CHANNELS) {
|
|
1262
1330
|
if (channelAt(channelSites[channel] % nativeWidth, Math.floor(channelSites[channel] / nativeWidth)) !== channel)
|
|
1263
|
-
fail("
|
|
1331
|
+
fail(caller + " needs at least a 2 × 2 native raster.");
|
|
1264
1332
|
const environment = { ...siteInput(channelSites[channel]).environment };
|
|
1265
1333
|
environment.psf = { kind: "not-applied", evidence, limitation: "Ideal reference: no lens PSF." };
|
|
1266
1334
|
environment.fieldThroughput = { kind: "unity", evidence, limitation: "Ideal reference: no vignetting." };
|
|
@@ -1288,13 +1356,28 @@ async function runCapture(options, idealReference) {
|
|
|
1288
1356
|
systemConversionGainElectronsPerCode: gain, blackLevelCode: black, digitalSaturationCode: regime.adc.digitalSaturationCode, adcMaximumCode: 2 ** regime.adc.bitDepth - 1, digitalGain }, readoutProfile, regimeId);
|
|
1289
1357
|
relativePerElectron[channel] = digitalGain / (gain * (white - black));
|
|
1290
1358
|
}
|
|
1359
|
+
return { electronsPerRadiance, wavelengths, relativePerElectron };
|
|
1360
|
+
}
|
|
1361
|
+
// Display view: the renderer that simulate-display-view.ts passes in reads this request's resolved state, so a bundle
|
|
1362
|
+
// that only captures does not contain the display view.
|
|
1363
|
+
if (displayView)
|
|
1364
|
+
return await renderView({ 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, kelvinWhiteBalanceInput });
|
|
1365
|
+
// Ideal reference (2.3): the scene without camera effects, at the capture's own exposure, white balance and encoding.
|
|
1366
|
+
if (idealReference) {
|
|
1367
|
+
const referenceStarted = clockMs();
|
|
1368
|
+
if (format !== "srgb-8" && format !== "srgb-16" && format !== "png")
|
|
1369
|
+
fail("simulateIdealReference supports output.format srgb-8, srgb-16 and png: a reference has no RAW (dng) and should not carry JPEG compression artefacts.");
|
|
1370
|
+
if (needsAuto)
|
|
1371
|
+
fail("simulateIdealReference cannot reproduce output.whiteBalance \"auto\": the capture measures it from its noisy RAW. Use \"scene-illuminant\" or a fixed white balance.");
|
|
1372
|
+
const { electronsPerRadiance, wavelengths, relativePerElectron } = axisChannelResponse("simulateIdealReference");
|
|
1291
1373
|
// 3. Scene radiance per sample: chart materials once (and the fixed-size chart's surround), caller functions per sample.
|
|
1292
1374
|
const signalOf = (radiance) => {
|
|
1293
1375
|
const values = new Map(wavelengths.map((nm) => [nm, radiance(nm)]));
|
|
1294
1376
|
return CHANNELS.map((c) => { let e = 0; for (const [nm, a] of electronsPerRadiance[c])
|
|
1295
1377
|
e += a * values.get(nm); return e * relativePerElectron[c]; });
|
|
1296
1378
|
};
|
|
1297
|
-
const
|
|
1379
|
+
const materialCount = chart ? chart.materials.length : callerScene?.kind === "material-function" ? callerScene.materials.length : 0;
|
|
1380
|
+
const materialSignals = provider.chartMaterialAt ? Array.from({ length: materialCount + (chartSize ? 1 : 0) }, (_, m) => signalOf((nm) => provider.materialRadiance(m, nm))) : undefined;
|
|
1298
1381
|
// 4. Pixels: the native sites of the output (or region) pixels, each centre-sampled or box-integrated like the capture.
|
|
1299
1382
|
const x0 = (region ? region.x : 0) + margin, y0 = (region ? region.y : 0) + margin, n = boxSamples, unit = 2 / width;
|
|
1300
1383
|
const referenceIntent = intentOf(whiteBalanceState), referenceGains = referenceIntent ? [referenceIntent.channelGains.red, referenceIntent.channelGains.green, referenceIntent.channelGains.blue] : null;
|