@photivra/engine 0.2.0 → 0.4.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 +68 -0
- package/CITATION.cff +1 -1
- package/README.md +58 -10
- package/dist/camera/equivalent-focal-length.d.ts +34 -0
- package/dist/camera/equivalent-focal-length.d.ts.map +1 -0
- package/dist/camera/equivalent-focal-length.js +33 -0
- package/dist/camera/equivalent-focal-length.js.map +1 -0
- package/dist/camera/field-of-view.d.ts +40 -3
- package/dist/camera/field-of-view.d.ts.map +1 -1
- package/dist/camera/field-of-view.js +66 -24
- package/dist/camera/field-of-view.js.map +1 -1
- package/dist/core/calculation-result.d.ts.map +1 -1
- package/dist/core/calculation-result.js +14 -2
- package/dist/core/calculation-result.js.map +1 -1
- package/dist/core/configuration-error.d.ts +9 -0
- package/dist/core/configuration-error.d.ts.map +1 -0
- package/dist/core/configuration-error.js +13 -0
- package/dist/core/configuration-error.js.map +1 -0
- package/dist/core/evidence-provenance.d.ts +40 -0
- package/dist/core/evidence-provenance.d.ts.map +1 -0
- package/dist/core/evidence-provenance.js +79 -0
- package/dist/core/evidence-provenance.js.map +1 -0
- package/dist/core/image-formation.d.ts +70 -0
- package/dist/core/image-formation.d.ts.map +1 -0
- package/dist/core/image-formation.js +335 -0
- package/dist/core/image-formation.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/dist/motion/camera-rotation.d.ts +91 -0
- package/dist/motion/camera-rotation.d.ts.map +1 -0
- package/dist/motion/camera-rotation.js +157 -0
- package/dist/motion/camera-rotation.js.map +1 -0
- package/dist/optics/focus-breathing.d.ts +73 -0
- package/dist/optics/focus-breathing.d.ts.map +1 -0
- package/dist/optics/focus-breathing.js +77 -0
- package/dist/optics/focus-breathing.js.map +1 -0
- package/dist/optics/illumination-vignetting.d.ts +59 -0
- package/dist/optics/illumination-vignetting.d.ts.map +1 -0
- package/dist/optics/illumination-vignetting.js +107 -0
- package/dist/optics/illumination-vignetting.js.map +1 -0
- package/dist/optics/lateral-chromatic-aberration.d.ts +93 -0
- package/dist/optics/lateral-chromatic-aberration.d.ts.map +1 -0
- package/dist/optics/lateral-chromatic-aberration.js +174 -0
- package/dist/optics/lateral-chromatic-aberration.js.map +1 -0
- package/dist/optics/psf-foundation.d.ts +102 -0
- package/dist/optics/psf-foundation.d.ts.map +1 -0
- package/dist/optics/psf-foundation.js +164 -0
- package/dist/optics/psf-foundation.js.map +1 -0
- package/dist/optics/radial-distortion.d.ts +91 -0
- package/dist/optics/radial-distortion.d.ts.map +1 -0
- package/dist/optics/radial-distortion.js +198 -0
- package/dist/optics/radial-distortion.js.map +1 -0
- package/dist/output/capture-geometry.d.ts +225 -0
- package/dist/output/capture-geometry.d.ts.map +1 -0
- package/dist/output/capture-geometry.js +478 -0
- package/dist/output/capture-geometry.js.map +1 -0
- package/dist/schema/validation.d.ts +1 -8
- package/dist/schema/validation.d.ts.map +1 -1
- package/dist/schema/validation.js +2 -11
- package/dist/schema/validation.js.map +1 -1
- package/dist/sensor/architecture.d.ts +40 -0
- package/dist/sensor/architecture.d.ts.map +1 -0
- package/dist/sensor/architecture.js +98 -0
- package/dist/sensor/architecture.js.map +1 -0
- package/dist/sensor/radiometry-readiness.d.ts +111 -0
- package/dist/sensor/radiometry-readiness.d.ts.map +1 -0
- package/dist/sensor/radiometry-readiness.js +294 -0
- package/dist/sensor/radiometry-readiness.js.map +1 -0
- package/dist/sensor/sensor-geometry.d.ts +99 -0
- package/dist/sensor/sensor-geometry.d.ts.map +1 -0
- package/dist/sensor/sensor-geometry.js +71 -0
- package/dist/sensor/sensor-geometry.js.map +1 -0
- package/dist/simulation/poc-simulation.d.ts +93 -1
- package/dist/simulation/poc-simulation.d.ts.map +1 -1
- package/dist/simulation/poc-simulation.js +308 -24
- package/dist/simulation/poc-simulation.js.map +1 -1
- package/docs/API_STYLE.md +35 -2
- package/docs/ARCHITECTURE.md +179 -1
- package/docs/IMAGE_FORMATION.md +142 -0
- package/docs/MOTION_AND_SIGNAL.md +52 -1
- package/docs/PHYSICS_FOUNDATION.md +190 -3
- package/docs/POC_API.md +73 -7
- package/docs/PROVENANCE.md +24 -0
- package/docs/PSF_FOUNDATION.md +133 -0
- package/docs/STABILIZATION.md +17 -4
- package/docs/USAGE.md +657 -3
- package/package.json +1 -1
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
import { calculatedResult } from "../core/calculation-result.js";
|
|
3
|
+
import { InvalidScientificInputError, requirePositiveFinite, requirePositiveInteger } from "../core/validation.js";
|
|
4
|
+
const REFERENCE_35MM_WIDTH_MM = 36;
|
|
5
|
+
const REFERENCE_35MM_HEIGHT_MM = 24;
|
|
6
|
+
const REFERENCE_35MM_DIAGONAL_MM = Math.hypot(REFERENCE_35MM_WIDTH_MM, REFERENCE_35MM_HEIGHT_MM);
|
|
7
|
+
/**
|
|
8
|
+
* Calculates physical imaging-area metrics independently from native raster
|
|
9
|
+
* density.
|
|
10
|
+
*
|
|
11
|
+
* Crop factor uses the diagonal ratio to a 36 × 24 mm reference frame.
|
|
12
|
+
* Digital/output crops are not part of this physical imaging-area quantity.
|
|
13
|
+
*
|
|
14
|
+
* @param imagingArea Physical photosensitive imaging area.
|
|
15
|
+
* @returns Physical diagonal, aspect ratio, and diagonal 35 mm crop factor.
|
|
16
|
+
*/
|
|
17
|
+
export function calculateImagingAreaMetrics(imagingArea) {
|
|
18
|
+
requirePositiveFinite("imagingArea.widthMm", imagingArea.widthMm);
|
|
19
|
+
requirePositiveFinite("imagingArea.heightMm", imagingArea.heightMm);
|
|
20
|
+
const diagonalMm = Math.hypot(imagingArea.widthMm, imagingArea.heightMm);
|
|
21
|
+
return calculatedResult({
|
|
22
|
+
diagonalMm,
|
|
23
|
+
aspectRatio: imagingArea.widthMm / imagingArea.heightMm,
|
|
24
|
+
cropFactor35Mm: REFERENCE_35MM_DIAGONAL_MM / diagonalMm
|
|
25
|
+
}, "sensor-imaging-area", "1.0.0", [
|
|
26
|
+
"35 mm reference frame is 36 × 24 mm and crop factor is diagonal-based.",
|
|
27
|
+
"Digital/output cropping is excluded from physical imaging-area crop factor."
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Calculates physical imaging-area, native-raster, and geometric sampling
|
|
32
|
+
* metrics without coupling sensor size to resolution.
|
|
33
|
+
*
|
|
34
|
+
* The supplied raster is assumed to span the supplied imaging area. Derived
|
|
35
|
+
* sampling pitch is geometric sample spacing only and must not be interpreted
|
|
36
|
+
* as photosite fill factor or photon-collection area.
|
|
37
|
+
*
|
|
38
|
+
* @param input Physical imaging area and native effective image raster.
|
|
39
|
+
* @returns Derived imaging-area, raster, and sampling metrics.
|
|
40
|
+
*/
|
|
41
|
+
export function calculateSensorGeometryMetrics(input) {
|
|
42
|
+
requirePositiveFinite("imagingArea.widthMm", input.imagingArea.widthMm);
|
|
43
|
+
requirePositiveFinite("imagingArea.heightMm", input.imagingArea.heightMm);
|
|
44
|
+
requirePositiveInteger("nativeRaster.pixelWidth", input.nativeRaster.pixelWidth);
|
|
45
|
+
requirePositiveInteger("nativeRaster.pixelHeight", input.nativeRaster.pixelHeight);
|
|
46
|
+
const totalImageSamples = input.nativeRaster.pixelWidth * input.nativeRaster.pixelHeight;
|
|
47
|
+
if (!Number.isSafeInteger(totalImageSamples)) {
|
|
48
|
+
throw new InvalidScientificInputError("nativeRaster total image-sample count must be a positive safe integer.");
|
|
49
|
+
}
|
|
50
|
+
const imagingAreaMetrics = calculateImagingAreaMetrics(input.imagingArea).value;
|
|
51
|
+
const pitchXMicrometers = (input.imagingArea.widthMm / input.nativeRaster.pixelWidth) * 1000;
|
|
52
|
+
const pitchYMicrometers = (input.imagingArea.heightMm / input.nativeRaster.pixelHeight) * 1000;
|
|
53
|
+
return calculatedResult({
|
|
54
|
+
imagingArea: imagingAreaMetrics,
|
|
55
|
+
nativeRaster: {
|
|
56
|
+
totalImageSamples,
|
|
57
|
+
megapixels: totalImageSamples / 1_000_000,
|
|
58
|
+
aspectRatio: input.nativeRaster.pixelWidth / input.nativeRaster.pixelHeight
|
|
59
|
+
},
|
|
60
|
+
sampling: {
|
|
61
|
+
pitchXMicrometers,
|
|
62
|
+
pitchYMicrometers,
|
|
63
|
+
pitchAspectRatio: pitchXMicrometers / pitchYMicrometers
|
|
64
|
+
}
|
|
65
|
+
}, "sensor-imaging-area-native-raster", "1.0.0", [
|
|
66
|
+
"35 mm reference frame is 36 × 24 mm and crop factor is diagonal-based.",
|
|
67
|
+
"The native effective image raster is assumed to span the supplied physical imaging area.",
|
|
68
|
+
"Derived sampling pitch is geometric image-sample spacing, not physical photodiode active area or fill factor."
|
|
69
|
+
]);
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=sensor-geometry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sensor-geometry.js","sourceRoot":"","sources":["../../src/sensor/sensor-geometry.ts"],"names":[],"mappings":"AAAA,sCAAsC;AAEtC,OAAO,EACL,gBAAgB,EAEjB,MAAM,+BAA+B,CAAC;AACvC,OAAO,EACL,2BAA2B,EAC3B,qBAAqB,EACrB,sBAAsB,EACvB,MAAM,uBAAuB,CAAC;AAE/B,MAAM,uBAAuB,GAAG,EAAE,CAAC;AACnC,MAAM,wBAAwB,GAAG,EAAE,CAAC;AACpC,MAAM,0BAA0B,GAAG,IAAI,CAAC,KAAK,CAC3C,uBAAuB,EACvB,wBAAwB,CACzB,CAAC;AAkFF;;;;;;;;;GASG;AACH,MAAM,UAAU,2BAA2B,CACzC,WAA8B;IAE9B,qBAAqB,CAAC,qBAAqB,EAAE,WAAW,CAAC,OAAO,CAAC,CAAC;IAClE,qBAAqB,CAAC,sBAAsB,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEpE,MAAM,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;IAEzE,OAAO,gBAAgB,CACrB;QACE,UAAU;QACV,WAAW,EAAE,WAAW,CAAC,OAAO,GAAG,WAAW,CAAC,QAAQ;QACvD,cAAc,EAAE,0BAA0B,GAAG,UAAU;KACxD,EACD,qBAAqB,EACrB,OAAO,EACP;QACE,wEAAwE;QACxE,6EAA6E;KAC9E,CACF,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,8BAA8B,CAC5C,KAA0C;IAE1C,qBAAqB,CAAC,qBAAqB,EAAE,KAAK,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC;IACxE,qBAAqB,CAAC,sBAAsB,EAAE,KAAK,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;IAC1E,sBAAsB,CACpB,yBAAyB,EACzB,KAAK,CAAC,YAAY,CAAC,UAAU,CAC9B,CAAC;IACF,sBAAsB,CACpB,0BAA0B,EAC1B,KAAK,CAAC,YAAY,CAAC,WAAW,CAC/B,CAAC;IAEF,MAAM,iBAAiB,GACrB,KAAK,CAAC,YAAY,CAAC,UAAU,GAAG,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC;IACjE,IAAI,CAAC,MAAM,CAAC,aAAa,CAAC,iBAAiB,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,2BAA2B,CACnC,wEAAwE,CACzE,CAAC;IACJ,CAAC;IAED,MAAM,kBAAkB,GAAG,2BAA2B,CACpD,KAAK,CAAC,WAAW,CAClB,CAAC,KAAK,CAAC;IACR,MAAM,iBAAiB,GACrB,CAAC,KAAK,CAAC,WAAW,CAAC,OAAO,GAAG,KAAK,CAAC,YAAY,CAAC,UAAU,CAAC,GAAG,IAAI,CAAC;IACrE,MAAM,iBAAiB,GACrB,CAAC,KAAK,CAAC,WAAW,CAAC,QAAQ,GAAG,KAAK,CAAC,YAAY,CAAC,WAAW,CAAC,GAAG,IAAI,CAAC;IAEvE,OAAO,gBAAgB,CACrB;QACE,WAAW,EAAE,kBAAkB;QAC/B,YAAY,EAAE;YACZ,iBAAiB;YACjB,UAAU,EAAE,iBAAiB,GAAG,SAAS;YACzC,WAAW,EACT,KAAK,CAAC,YAAY,CAAC,UAAU,GAAG,KAAK,CAAC,YAAY,CAAC,WAAW;SACjE;QACD,QAAQ,EAAE;YACR,iBAAiB;YACjB,iBAAiB;YACjB,gBAAgB,EAAE,iBAAiB,GAAG,iBAAiB;SACxD;KACF,EACD,mCAAmC,EACnC,OAAO,EACP;QACE,wEAAwE;QACxE,0FAA0F;QAC1F,+GAA+G;KAChH,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import type { CalculationProvenance } from "../core/calculation-result.js";
|
|
2
|
+
import { type ActiveCaptureFieldOfView, type CaptureOrientation, type OutputFieldOfView, type PhysicalBoundsFromOpticalAxisMm, type RasterRect, type RasterVector, type ResolvedCaptureGeometry } from "../output/capture-geometry.js";
|
|
3
|
+
import { type RasterDimensions, type SensorGeometryMetrics, type SensorImagingArea } from "../sensor/sensor-geometry.js";
|
|
2
4
|
import type { Vector3 } from "../schema/scene.js";
|
|
5
|
+
export declare const POC_SIMULATION_API_VERSION: "0.20.0";
|
|
3
6
|
export interface PocSimulationRequest {
|
|
4
7
|
sensor: {
|
|
5
8
|
widthMm: number;
|
|
@@ -25,8 +28,24 @@ export interface PocSimulationRequest {
|
|
|
25
28
|
};
|
|
26
29
|
};
|
|
27
30
|
crop: {
|
|
31
|
+
/**
|
|
32
|
+
* Legacy centered same-aspect crop factor.
|
|
33
|
+
*
|
|
34
|
+
* When capture geometry is supplied this must remain 1. New callers should
|
|
35
|
+
* use capture.activeCaptureRect/outputCropRect instead of stacking two
|
|
36
|
+
* independent crop models.
|
|
37
|
+
*/
|
|
28
38
|
factor: number;
|
|
29
39
|
};
|
|
40
|
+
/**
|
|
41
|
+
* Opt-in staged capture geometry. Omit for exact legacy POC behavior.
|
|
42
|
+
*/
|
|
43
|
+
capture?: {
|
|
44
|
+
orientation: CaptureOrientation;
|
|
45
|
+
activeCaptureRect?: RasterRect;
|
|
46
|
+
outputCropRect?: RasterRect;
|
|
47
|
+
outputRaster?: RasterDimensions;
|
|
48
|
+
};
|
|
30
49
|
diffraction: {
|
|
31
50
|
wavelengthNm: number;
|
|
32
51
|
};
|
|
@@ -77,8 +96,20 @@ interface FieldOfViewSummary {
|
|
|
77
96
|
verticalDegrees: number;
|
|
78
97
|
diagonalDegrees: number;
|
|
79
98
|
}
|
|
99
|
+
interface CaptureSubjectFraming {
|
|
100
|
+
additionalCropFactor: number;
|
|
101
|
+
raster: RasterDimensions;
|
|
102
|
+
megapixels: number;
|
|
103
|
+
subjectHeightFraction: number;
|
|
104
|
+
subjectClipped: boolean;
|
|
105
|
+
additionalCropApplied: boolean;
|
|
106
|
+
retainedImagingArea: SensorImagingArea;
|
|
107
|
+
physicalBoundsFromOpticalAxisMm: PhysicalBoundsFromOpticalAxisMm;
|
|
108
|
+
effectiveFieldOfView: FieldOfViewSummary;
|
|
109
|
+
basis: "centered-output-framing";
|
|
110
|
+
}
|
|
80
111
|
export interface PocSimulationResponse {
|
|
81
|
-
apiVersion:
|
|
112
|
+
apiVersion: typeof POC_SIMULATION_API_VERSION;
|
|
82
113
|
projection: {
|
|
83
114
|
kind: "focus-aware-thin-lens";
|
|
84
115
|
focusDistanceM: number;
|
|
@@ -95,7 +126,66 @@ export interface PocSimulationResponse {
|
|
|
95
126
|
diagonalDegrees: number;
|
|
96
127
|
};
|
|
97
128
|
sensor: {
|
|
129
|
+
/** Physical imaging-area/native-raster metrics from the shared sensor foundation. */
|
|
130
|
+
geometry: SensorGeometryMetrics;
|
|
131
|
+
/**
|
|
132
|
+
* Backwards-compatible representative pitch used by the current composed
|
|
133
|
+
* POC calculations. This remains the horizontal pitch.
|
|
134
|
+
*/
|
|
98
135
|
pixelPitchMicrometers: number;
|
|
136
|
+
pitchXMicrometers: number;
|
|
137
|
+
pitchYMicrometers: number;
|
|
138
|
+
pitchAxisRelativeDifference: number;
|
|
139
|
+
};
|
|
140
|
+
capture?: {
|
|
141
|
+
geometry: ResolvedCaptureGeometry;
|
|
142
|
+
activeFieldOfView: ActiveCaptureFieldOfView;
|
|
143
|
+
outputFieldOfView: OutputFieldOfView;
|
|
144
|
+
focalLength: {
|
|
145
|
+
actualFocalLengthMm: number;
|
|
146
|
+
equivalentFocalLength35Mm: number;
|
|
147
|
+
cropFactor35Mm: number;
|
|
148
|
+
activeImagingAreaDiagonalMm: number;
|
|
149
|
+
basis: "diagonal";
|
|
150
|
+
};
|
|
151
|
+
/**
|
|
152
|
+
* Axis-aware pixel-domain mapping from oriented active-capture samples to
|
|
153
|
+
* the declared output raster. Subject framing is a later crop, not another
|
|
154
|
+
* resampling step.
|
|
155
|
+
*/
|
|
156
|
+
outputSamplingScale: {
|
|
157
|
+
x: number;
|
|
158
|
+
y: number;
|
|
159
|
+
axisRelativeDifference: number;
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* Additional post-output subject framing. This does not mutate physical
|
|
163
|
+
* sensor identity, active capture, or active-capture focal equivalence.
|
|
164
|
+
*/
|
|
165
|
+
subjectFraming?: CaptureSubjectFraming;
|
|
166
|
+
motion: {
|
|
167
|
+
nativeRasterDeltaPixels: RasterVector;
|
|
168
|
+
orientedCaptureDeltaPixels: RasterVector;
|
|
169
|
+
outputDeltaPixels: RasterVector;
|
|
170
|
+
};
|
|
171
|
+
motionSamples?: readonly {
|
|
172
|
+
id: string;
|
|
173
|
+
nativeRasterDeltaPixels: RasterVector;
|
|
174
|
+
orientedCaptureDeltaPixels: RasterVector;
|
|
175
|
+
outputDeltaPixels: RasterVector;
|
|
176
|
+
}[];
|
|
177
|
+
cameraShake?: {
|
|
178
|
+
unstabilized: {
|
|
179
|
+
nativeRasterDeltaPixels: RasterVector;
|
|
180
|
+
orientedCaptureDeltaPixels: RasterVector;
|
|
181
|
+
outputDeltaPixels: RasterVector;
|
|
182
|
+
};
|
|
183
|
+
stabilized: {
|
|
184
|
+
nativeRasterDeltaPixels: RasterVector;
|
|
185
|
+
orientedCaptureDeltaPixels: RasterVector;
|
|
186
|
+
outputDeltaPixels: RasterVector;
|
|
187
|
+
};
|
|
188
|
+
};
|
|
99
189
|
};
|
|
100
190
|
crop: {
|
|
101
191
|
cropFactor: number;
|
|
@@ -112,6 +202,8 @@ export interface PocSimulationResponse {
|
|
|
112
202
|
source: "equivalent-viewing-approximation";
|
|
113
203
|
circleOfConfusionMm: number;
|
|
114
204
|
scaleFactor: number;
|
|
205
|
+
targetBasis: "full-sensor" | "final-retained-output";
|
|
206
|
+
targetImagingArea: SensorImagingArea;
|
|
115
207
|
provenance: {
|
|
116
208
|
kind: "approximation";
|
|
117
209
|
model: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"poc-simulation.d.ts","sourceRoot":"","sources":["../../src/simulation/poc-simulation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"poc-simulation.d.ts","sourceRoot":"","sources":["../../src/simulation/poc-simulation.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,+BAA+B,CAAC;AAe3E,OAAO,EAKL,KAAK,wBAAwB,EAC7B,KAAK,kBAAkB,EACvB,KAAK,iBAAiB,EACtB,KAAK,+BAA+B,EACpC,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,uBAAuB,EAC7B,MAAM,+BAA+B,CAAC;AAIvC,OAAO,EAEL,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAC1B,KAAK,iBAAiB,EACvB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,oBAAoB,CAAC;AAGlD,eAAO,MAAM,0BAA0B,EAAG,QAAiB,CAAC;AAI5D,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE;QACN,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;KACrB,CAAC;IACF,IAAI,EAAE;QACJ,aAAa,EAAE,MAAM,CAAC;QACtB,QAAQ,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,QAAQ,EAAE;QACR,cAAc,EAAE,MAAM,CAAC;QACvB,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,KAAK,EAAE;QACL,cAAc,EAAE,MAAM,CAAC;QACvB,mBAAmB,CAAC,EAAE,MAAM,CAAC;QAC7B,kCAAkC,CAAC,EAAE;YACnC,sBAAsB,EAAE,MAAM,CAAC;YAC/B,uBAAuB,EAAE,MAAM,CAAC;YAChC,4BAA4B,EAAE,MAAM,CAAC;SACtC,CAAC;KACH,CAAC;IACF,IAAI,EAAE;QACJ;;;;;;WAMG;QACH,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;IACF;;OAEG;IACH,OAAO,CAAC,EAAE;QACR,WAAW,EAAE,kBAAkB,CAAC;QAChC,iBAAiB,CAAC,EAAE,UAAU,CAAC;QAC/B,cAAc,CAAC,EAAE,UAAU,CAAC;QAC5B,YAAY,CAAC,EAAE,gBAAgB,CAAC;KACjC,CAAC;IACF,WAAW,EAAE;QACX,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,MAAM,EAAE;QACN,SAAS,EAAE,OAAO,CAAC;QACnB,WAAW,EAAE,OAAO,CAAC;KACtB,CAAC;IACF,OAAO,CAAC,EAAE;QACR,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,cAAc,CAAC,EAAE,SAAS;QACxB,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;KACnB,EAAE,CAAC;IACJ,eAAe,CAAC,EAAE,SAAS;QACzB,EAAE,EAAE,MAAM,CAAC;QACX,MAAM,EAAE,MAAM,CAAC;QACf,OAAO,EAAE,MAAM,CAAC;QAChB,SAAS,EAAE,MAAM,CAAC;KACnB,EAAE,CAAC;IACJ,aAAa,CAAC,EAAE,SAAS;QACvB,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,OAAO,CAAC;QACnB,WAAW,EAAE,OAAO,CAAC;KACtB,EAAE,CAAC;IACJ,WAAW,CAAC,EAAE;QACZ,2BAA2B,EAAE,MAAM,CAAC;KACrC,CAAC;IACF,WAAW,CAAC,EAAE;QACZ,wBAAwB,EAAE;YACxB,GAAG,EAAE,MAAM,CAAC;YACZ,KAAK,EAAE,MAAM,CAAC;SACf,CAAC;QACF,4BAA4B,EAAE,MAAM,CAAC;KACtC,CAAC;IACF,aAAa,CAAC,EAAE;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,0BAA0B,CAAC,EAAE,MAAM,CAAC;KACrC,CAAC;IACF,WAAW,CAAC,EAAE;QACZ,qBAAqB,CAAC,EAAE,MAAM,CAAC;KAChC,CAAC;CACH;AAsDD,UAAU,kBAAkB;IAC1B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,eAAe,EAAE,MAAM,CAAC;IACxB,eAAe,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,qBAAqB;IAC7B,oBAAoB,EAAE,MAAM,CAAC;IAC7B,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,MAAM,CAAC;IACnB,qBAAqB,EAAE,MAAM,CAAC;IAC9B,cAAc,EAAE,OAAO,CAAC;IACxB,qBAAqB,EAAE,OAAO,CAAC;IAC/B,mBAAmB,EAAE,iBAAiB,CAAC;IACvC,+BAA+B,EAAE,+BAA+B,CAAC;IACjE,oBAAoB,EAAE,kBAAkB,CAAC;IACzC,KAAK,EAAE,yBAAyB,CAAC;CAClC;AAyFD,MAAM,WAAW,qBAAqB;IACpC,UAAU,EAAE,OAAO,0BAA0B,CAAC;IAC9C,UAAU,EAAE;QACV,IAAI,EAAE,uBAAuB,CAAC;QAC9B,cAAc,EAAE,MAAM,CAAC;QACvB,eAAe,EAAE,MAAM,CAAC;QACxB,uBAAuB,EAAE,MAAM,CAAC;QAChC,UAAU,EAAE,qBAAqB,CAAC;KACnC,CAAC;IACF;;OAEG;IACH,WAAW,EAAE;QACX,iBAAiB,EAAE,MAAM,CAAC;QAC1B,eAAe,EAAE,MAAM,CAAC;QACxB,eAAe,EAAE,MAAM,CAAC;KACzB,CAAC;IACF,MAAM,EAAE;QACN,qFAAqF;QACrF,QAAQ,EAAE,qBAAqB,CAAC;QAChC;;;WAGG;QACH,qBAAqB,EAAE,MAAM,CAAC;QAC9B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,iBAAiB,EAAE,MAAM,CAAC;QAC1B,2BAA2B,EAAE,MAAM,CAAC;KACrC,CAAC;IACF,OAAO,CAAC,EAAE;QACR,QAAQ,EAAE,uBAAuB,CAAC;QAClC,iBAAiB,EAAE,wBAAwB,CAAC;QAC5C,iBAAiB,EAAE,iBAAiB,CAAC;QACrC,WAAW,EAAE;YACX,mBAAmB,EAAE,MAAM,CAAC;YAC5B,yBAAyB,EAAE,MAAM,CAAC;YAClC,cAAc,EAAE,MAAM,CAAC;YACvB,2BAA2B,EAAE,MAAM,CAAC;YACpC,KAAK,EAAE,UAAU,CAAC;SACnB,CAAC;QACF;;;;WAIG;QACH,mBAAmB,EAAE;YACnB,CAAC,EAAE,MAAM,CAAC;YACV,CAAC,EAAE,MAAM,CAAC;YACV,sBAAsB,EAAE,MAAM,CAAC;SAChC,CAAC;QACF;;;WAGG;QACH,cAAc,CAAC,EAAE,qBAAqB,CAAC;QACvC,MAAM,EAAE;YACN,uBAAuB,EAAE,YAAY,CAAC;YACtC,0BAA0B,EAAE,YAAY,CAAC;YACzC,iBAAiB,EAAE,YAAY,CAAC;SACjC,CAAC;QACF,aAAa,CAAC,EAAE,SAAS;YACvB,EAAE,EAAE,MAAM,CAAC;YACX,uBAAuB,EAAE,YAAY,CAAC;YACtC,0BAA0B,EAAE,YAAY,CAAC;YACzC,iBAAiB,EAAE,YAAY,CAAC;SACjC,EAAE,CAAC;QACJ,WAAW,CAAC,EAAE;YACZ,YAAY,EAAE;gBACZ,uBAAuB,EAAE,YAAY,CAAC;gBACtC,0BAA0B,EAAE,YAAY,CAAC;gBACzC,iBAAiB,EAAE,YAAY,CAAC;aACjC,CAAC;YACF,UAAU,EAAE;gBACV,uBAAuB,EAAE,YAAY,CAAC;gBACtC,0BAA0B,EAAE,YAAY,CAAC;gBACzC,iBAAiB,EAAE,YAAY,CAAC;aACjC,CAAC;SACH,CAAC;KACH,CAAC;IACF,IAAI,EAAE;QACJ,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,oBAAoB,EAAE,kBAAkB,CAAC;KAC1C,CAAC;IACF,cAAc,EACV;QACE,MAAM,EAAE,UAAU,CAAC;QACnB,mBAAmB,EAAE,MAAM,CAAC;KAC7B,GACD;QACE,MAAM,EAAE,kCAAkC,CAAC;QAC3C,mBAAmB,EAAE,MAAM,CAAC;QAC5B,WAAW,EAAE,MAAM,CAAC;QACpB,WAAW,EAAE,aAAa,GAAG,uBAAuB,CAAC;QACrD,iBAAiB,EAAE,iBAAiB,CAAC;QACrC,UAAU,EAAE;YACV,IAAI,EAAE,eAAe,CAAC;YACtB,KAAK,EAAE,MAAM,CAAC;YACd,YAAY,EAAE,MAAM,CAAC;YACrB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;SACjC,CAAC;KACH,CAAC;IACN,YAAY,EAAE;QACZ,mBAAmB,EAAE,MAAM,CAAC;QAC5B,UAAU,EAAE,MAAM,CAAC;QACnB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;QACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;KACnC,CAAC;IACF,WAAW,EAAE;QACX,4BAA4B,EAAE,MAAM,CAAC;QACrC,kBAAkB,EAAE,MAAM,CAAC;QAC3B,UAAU,EAAE,gBAAgB,CAAC;QAC7B,UAAU,EAAE,qBAAqB,CAAC;KACnC,CAAC;IACF,MAAM,EAAE;QACN,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,QAAQ,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,eAAe,CAAC,EAAE;QAChB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACtB,CAAC;IACF,cAAc,CAAC,EAAE,SAAS;QACxB,EAAE,EAAE,MAAM,CAAC;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;KACxB,EAAE,CAAC;IACJ,eAAe,CAAC,EAAE,SAAS;QACzB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE,MAAM,CAAC;QACpB,YAAY,EAAE,MAAM,CAAC;KACtB,EAAE,CAAC;IACJ,aAAa,CAAC,EAAE,SAAS;QACvB,EAAE,EAAE,MAAM,CAAC;QACX,UAAU,EAAE,MAAM,CAAC;QACnB,cAAc,EAAE,MAAM,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,QAAQ,EAAE,MAAM,CAAC;QACjB,YAAY,EAAE,MAAM,CAAC;QACrB,YAAY,EAAE,MAAM,CAAC;KACtB,EAAE,CAAC;IACJ,WAAW,CAAC,EAAE;QACZ,oEAAoE;QACpE,oBAAoB,EAAE,MAAM,CAAC;QAC7B,qDAAqD;QACrD,eAAe,EAAE,MAAM,CAAC;QACxB,UAAU,EAAE,MAAM,CAAC;QACnB,WAAW,EAAE,MAAM,CAAC;QACpB,UAAU,EAAE,MAAM,CAAC;QACnB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,cAAc,EAAE,OAAO,CAAC;QACxB,qBAAqB,EAAE,OAAO,CAAC;QAC/B,oBAAoB,EAAE,kBAAkB,CAAC;KAC1C,CAAC;IACF,aAAa,CAAC,EAAE;QACd,UAAU,EAAE,MAAM,CAAC;QACnB,eAAe,EAAE,MAAM,CAAC;QACxB,uBAAuB,EAAE,SAAS,MAAM,EAAE,CAAC;QAC3C,kBAAkB,EAAE,SAAS;YAC3B,CAAC,EAAE,MAAM,CAAC;YACV,CAAC,EAAE,MAAM,CAAC;SACX,EAAE,CAAC;QACJ,UAAU,EAAE,qBAAqB,CAAC;KACnC,CAAC;IACF,yBAAyB,CAAC,EAAE;QAC1B,gBAAgB,EAAE,MAAM,CAAC;QACzB,oBAAoB,EAAE,MAAM,CAAC;QAC7B,qBAAqB,EAAE,MAAM,CAAC;QAC9B,kCAAkC,EAAE,MAAM,CAAC;QAC3C,gBAAgB,EAAE,MAAM,CAAC;QACzB,aAAa,CAAC,EAAE;YACd,EAAE,EAAE,MAAM,CAAC;YACX,cAAc,EAAE,MAAM,CAAC;SACxB,CAAC;QACF,WAAW,CAAC,EAAE;YACZ,wBAAwB,EAAE,MAAM,CAAC;YACjC,cAAc,EAAE,eAAe,CAAC;SACjC,CAAC;QACF,iBAAiB,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,WAAW,CAAC,EAAE;QACZ,oBAAoB,EAAE,MAAM,CAAC;QAC7B,YAAY,EAAE;YACZ,QAAQ,EAAE,MAAM,CAAC;YACjB,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,cAAc,EAAE,MAAM,CAAC;YACvB,YAAY,EAAE,MAAM,CAAC;YACrB,YAAY,EAAE,MAAM,CAAC;SACtB,CAAC;QACF,UAAU,EAAE;YACV,QAAQ,EAAE,MAAM,CAAC;YACjB,QAAQ,EAAE,MAAM,CAAC;YACjB,UAAU,EAAE,MAAM,CAAC;YACnB,cAAc,EAAE,MAAM,CAAC;YACvB,YAAY,EAAE,MAAM,CAAC;YACrB,YAAY,EAAE,MAAM,CAAC;SACtB,CAAC;QACF,UAAU,EAAE;YACV,IAAI,EAAE,eAAe,CAAC;YACtB,KAAK,EAAE,MAAM,CAAC;YACd,YAAY,EAAE,MAAM,CAAC;YACrB,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;SACjC,CAAC;KACH,CAAC;IACF,UAAU,EAAE;QACV,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC;QAC7B,UAAU,EAAE;YACV,UAAU,EAAE,YAAY,CAAC;YACzB,WAAW,EAAE,YAAY,CAAC;YAC1B,IAAI,EAAE,YAAY,CAAC;YACnB,cAAc,EAAE,YAAY,GAAG,eAAe,CAAC;YAC/C,YAAY,EAAE,YAAY,CAAC;YAC3B,WAAW,EAAE,YAAY,CAAC;YAC1B,MAAM,EAAE,YAAY,CAAC;YACrB,QAAQ,EAAE,YAAY,CAAC;YACvB,aAAa,CAAC,EAAE,YAAY,CAAC;YAC7B,WAAW,CAAC,EAAE,eAAe,CAAC;SAC/B,CAAC;QACF,IAAI,EAAE,MAAM,CAAC;KACd,CAAC;CACH;AAoCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,oBAAoB,GAC5B,qBAAqB,CAwvBvB"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
import {
|
|
2
|
+
import { calculateEquivalentFocalLength35Mm } from "../camera/equivalent-focal-length.js";
|
|
3
|
+
import { calculateFieldOfView, calculateFieldOfViewBounds } from "../camera/field-of-view.js";
|
|
3
4
|
import { calculateProjectedObjectSize } from "../camera/projected-object-size.js";
|
|
4
5
|
import { InvalidScientificInputError, requirePositiveFinite } from "../core/validation.js";
|
|
5
6
|
import { calculateExposureValue100 } from "../exposure/exposure.js";
|
|
@@ -9,10 +10,14 @@ import { estimateEquivalentViewingCircleOfConfusion } from "../optics/circle-of-
|
|
|
9
10
|
import { calculateAiryDisk } from "../optics/diffraction.js";
|
|
10
11
|
import { calculateThinLensImageDistance } from "../optics/thin-lens.js";
|
|
11
12
|
import { calculateDefocusCircle, calculateDepthOfField } from "../optics/depth-of-field.js";
|
|
13
|
+
import { calculateActiveCaptureFieldOfView, calculateOutputFieldOfView, resolveCaptureGeometry, transformNativeRasterVectorToOriented } from "../output/capture-geometry.js";
|
|
12
14
|
import { calculateCenteredCrop } from "../output/crop.js";
|
|
13
15
|
import { calculateSubjectFramingCrop } from "../output/subject-framing-crop.js";
|
|
14
16
|
import { calculatePixelPitch } from "../sensor/pixel-pitch.js";
|
|
17
|
+
import { calculateSensorGeometryMetrics } from "../sensor/sensor-geometry.js";
|
|
15
18
|
import { estimateCameraShakeBlur } from "../stabilization/camera-shake.js";
|
|
19
|
+
export const POC_SIMULATION_API_VERSION = "0.20.0";
|
|
20
|
+
const POC_MAX_PITCH_AXIS_RELATIVE_DIFFERENCE = 0.01;
|
|
16
21
|
function calculateCroppedFieldOfView(sensorWidthMm, sensorHeightMm, focalLengthMm, focusDistanceM, cropFactor) {
|
|
17
22
|
const diagonalMm = Math.hypot(sensorWidthMm, sensorHeightMm);
|
|
18
23
|
return {
|
|
@@ -49,6 +54,80 @@ function requireUniqueSampleIds(label, samples) {
|
|
|
49
54
|
seen.add(normalizedId);
|
|
50
55
|
}
|
|
51
56
|
}
|
|
57
|
+
function centeredPhysicalCropBounds(bounds, retainedFractionX, retainedFractionY) {
|
|
58
|
+
const centerX = (bounds.left + bounds.right) / 2;
|
|
59
|
+
const centerY = (bounds.top + bounds.bottom) / 2;
|
|
60
|
+
const halfWidth = ((bounds.right - bounds.left) * retainedFractionX) / 2;
|
|
61
|
+
const halfHeight = ((bounds.bottom - bounds.top) * retainedFractionY) / 2;
|
|
62
|
+
return {
|
|
63
|
+
left: centerX - halfWidth,
|
|
64
|
+
right: centerX + halfWidth,
|
|
65
|
+
top: centerY - halfHeight,
|
|
66
|
+
bottom: centerY + halfHeight
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function calculateFieldOfViewForPhysicalBounds(bounds, focalLengthMm, focusDistanceM) {
|
|
70
|
+
const horizontal = calculateFieldOfViewBounds({
|
|
71
|
+
focalLengthMm,
|
|
72
|
+
minimumSensorCoordinateMm: bounds.left,
|
|
73
|
+
maximumSensorCoordinateMm: bounds.right,
|
|
74
|
+
focusDistanceM
|
|
75
|
+
}).value;
|
|
76
|
+
const vertical = calculateFieldOfViewBounds({
|
|
77
|
+
focalLengthMm,
|
|
78
|
+
minimumSensorCoordinateMm: bounds.top,
|
|
79
|
+
maximumSensorCoordinateMm: bounds.bottom,
|
|
80
|
+
focusDistanceM
|
|
81
|
+
}).value;
|
|
82
|
+
const projectionDistanceMm = horizontal.projectionDistanceMm;
|
|
83
|
+
const corners = {
|
|
84
|
+
topLeft: { x: bounds.left, y: bounds.top },
|
|
85
|
+
topRight: { x: bounds.right, y: bounds.top },
|
|
86
|
+
bottomRight: { x: bounds.right, y: bounds.bottom },
|
|
87
|
+
bottomLeft: { x: bounds.left, y: bounds.bottom }
|
|
88
|
+
};
|
|
89
|
+
const angleBetween = (first, second) => {
|
|
90
|
+
const firstLength = Math.hypot(first.x, first.y, projectionDistanceMm);
|
|
91
|
+
const secondLength = Math.hypot(second.x, second.y, projectionDistanceMm);
|
|
92
|
+
const dot = first.x * second.x +
|
|
93
|
+
first.y * second.y +
|
|
94
|
+
projectionDistanceMm * projectionDistanceMm;
|
|
95
|
+
const cosine = Math.max(-1, Math.min(1, dot / (firstLength * secondLength)));
|
|
96
|
+
return (Math.acos(cosine) * 180) / Math.PI;
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
horizontalDegrees: horizontal.degrees,
|
|
100
|
+
verticalDegrees: vertical.degrees,
|
|
101
|
+
diagonalDegrees: Math.max(angleBetween(corners.topLeft, corners.bottomRight), angleBetween(corners.topRight, corners.bottomLeft))
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function isPortraitOrientation(orientation) {
|
|
105
|
+
return (orientation === "portrait-clockwise" ||
|
|
106
|
+
orientation === "portrait-counter-clockwise");
|
|
107
|
+
}
|
|
108
|
+
function resolveCaptureVector(legacyImagePlaneVector, orientation, geometry) {
|
|
109
|
+
// Legacy projected-motion/camera-shake components use +X right and +Y up.
|
|
110
|
+
// Capture raster coordinates use +X right and +Y down. Preserve the legacy
|
|
111
|
+
// fields unchanged and convert explicitly before applying physical rotation.
|
|
112
|
+
const nativeRasterDeltaPixels = {
|
|
113
|
+
x: legacyImagePlaneVector.x,
|
|
114
|
+
y: -legacyImagePlaneVector.y
|
|
115
|
+
};
|
|
116
|
+
const orientedCaptureDeltaPixels = transformNativeRasterVectorToOriented({
|
|
117
|
+
vector: nativeRasterDeltaPixels,
|
|
118
|
+
orientation
|
|
119
|
+
});
|
|
120
|
+
const scaleX = geometry.output.orientedCaptureToOutputScale.x;
|
|
121
|
+
const scaleY = geometry.output.orientedCaptureToOutputScale.y;
|
|
122
|
+
return {
|
|
123
|
+
nativeRasterDeltaPixels,
|
|
124
|
+
orientedCaptureDeltaPixels,
|
|
125
|
+
outputDeltaPixels: {
|
|
126
|
+
x: orientedCaptureDeltaPixels.x * scaleX,
|
|
127
|
+
y: orientedCaptureDeltaPixels.y * scaleY
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
}
|
|
52
131
|
/**
|
|
53
132
|
* Composes the validated POC calculations into a single renderer/agent-facing
|
|
54
133
|
* response without introducing any new photographic model.
|
|
@@ -59,6 +138,48 @@ function requireUniqueSampleIds(label, samples) {
|
|
|
59
138
|
export function simulatePocCamera(request) {
|
|
60
139
|
requirePositiveFinite("sensor.widthMm", request.sensor.widthMm);
|
|
61
140
|
requirePositiveFinite("sensor.heightMm", request.sensor.heightMm);
|
|
141
|
+
const sensorGeometry = calculateSensorGeometryMetrics({
|
|
142
|
+
imagingArea: {
|
|
143
|
+
widthMm: request.sensor.widthMm,
|
|
144
|
+
heightMm: request.sensor.heightMm
|
|
145
|
+
},
|
|
146
|
+
nativeRaster: {
|
|
147
|
+
pixelWidth: request.sensor.pixelWidth,
|
|
148
|
+
pixelHeight: request.sensor.pixelHeight
|
|
149
|
+
}
|
|
150
|
+
}).value;
|
|
151
|
+
const pitchXMicrometers = sensorGeometry.sampling.pitchXMicrometers;
|
|
152
|
+
const pitchYMicrometers = sensorGeometry.sampling.pitchYMicrometers;
|
|
153
|
+
const pitchAxisRelativeDifference = Math.abs(pitchXMicrometers - pitchYMicrometers) /
|
|
154
|
+
((pitchXMicrometers + pitchYMicrometers) / 2);
|
|
155
|
+
if (pitchAxisRelativeDifference >
|
|
156
|
+
POC_MAX_PITCH_AXIS_RELATIVE_DIFFERENCE) {
|
|
157
|
+
throw new InvalidScientificInputError("The composed POC currently requires approximately square geometric sampling; X/Y pitch differ by more than 1%. Use lower-level axis-aware engine primitives until the POC contract supports separate X/Y sampling.");
|
|
158
|
+
}
|
|
159
|
+
const capture = request.capture;
|
|
160
|
+
const subjectCropRequest = request.subjectCrop;
|
|
161
|
+
const captureGeometry = capture === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: resolveCaptureGeometry({
|
|
164
|
+
imagingArea: {
|
|
165
|
+
widthMm: request.sensor.widthMm,
|
|
166
|
+
heightMm: request.sensor.heightMm
|
|
167
|
+
},
|
|
168
|
+
nativeRaster: {
|
|
169
|
+
pixelWidth: request.sensor.pixelWidth,
|
|
170
|
+
pixelHeight: request.sensor.pixelHeight
|
|
171
|
+
},
|
|
172
|
+
orientation: capture.orientation,
|
|
173
|
+
...(capture.activeCaptureRect === undefined
|
|
174
|
+
? {}
|
|
175
|
+
: { activeCaptureRect: capture.activeCaptureRect }),
|
|
176
|
+
...(capture.outputCropRect === undefined
|
|
177
|
+
? {}
|
|
178
|
+
: { outputCropRect: capture.outputCropRect }),
|
|
179
|
+
...(capture.outputRaster === undefined
|
|
180
|
+
? {}
|
|
181
|
+
: { outputRaster: capture.outputRaster })
|
|
182
|
+
}).value;
|
|
62
183
|
requirePositiveFinite("lens.focalLengthMm", request.lens.focalLengthMm);
|
|
63
184
|
requirePositiveFinite("lens.aperture", request.lens.aperture);
|
|
64
185
|
requirePositiveFinite("exposure.shutterSeconds", request.exposure.shutterSeconds);
|
|
@@ -69,20 +190,10 @@ export function simulatePocCamera(request) {
|
|
|
69
190
|
if (hasExplicitCircleOfConfusion === hasEquivalentViewingCriterion) {
|
|
70
191
|
throw new InvalidScientificInputError("Provide exactly one focus circle-of-confusion criterion.");
|
|
71
192
|
}
|
|
72
|
-
const equivalentViewingCircleOfConfusion = request.focus.equivalentViewingCircleOfConfusion === undefined
|
|
73
|
-
? undefined
|
|
74
|
-
: estimateEquivalentViewingCircleOfConfusion({
|
|
75
|
-
sensorWidthMm: request.sensor.widthMm,
|
|
76
|
-
sensorHeightMm: request.sensor.heightMm,
|
|
77
|
-
...request.focus.equivalentViewingCircleOfConfusion
|
|
78
|
-
});
|
|
79
|
-
const circleOfConfusionMm = equivalentViewingCircleOfConfusion?.value.circleOfConfusionMm ??
|
|
80
|
-
request.focus.circleOfConfusionMm;
|
|
81
|
-
if (circleOfConfusionMm === undefined) {
|
|
82
|
-
throw new InvalidScientificInputError("A focus circle-of-confusion criterion is required.");
|
|
83
|
-
}
|
|
84
|
-
requirePositiveFinite("focus.circleOfConfusionMm", circleOfConfusionMm);
|
|
85
193
|
requirePositiveFinite("crop.factor", request.crop.factor);
|
|
194
|
+
if (capture !== undefined && request.crop.factor !== 1) {
|
|
195
|
+
throw new InvalidScientificInputError("capture geometry cannot be combined with legacy crop.factor other than 1.");
|
|
196
|
+
}
|
|
86
197
|
requirePositiveFinite("diffraction.wavelengthNm", request.diffraction.wavelengthNm);
|
|
87
198
|
requireUniqueSampleIds("defocusSamples", request.defocusSamples);
|
|
88
199
|
requireUniqueSampleIds("samplingSamples", request.samplingSamples);
|
|
@@ -107,6 +218,51 @@ export function simulatePocCamera(request) {
|
|
|
107
218
|
sensorDimensionMm: diagonalMm,
|
|
108
219
|
focusDistanceM: request.focus.focusDistanceM
|
|
109
220
|
});
|
|
221
|
+
const activeCaptureFieldOfView = captureGeometry === undefined || capture === undefined
|
|
222
|
+
? undefined
|
|
223
|
+
: calculateActiveCaptureFieldOfView({
|
|
224
|
+
imagingArea: {
|
|
225
|
+
widthMm: request.sensor.widthMm,
|
|
226
|
+
heightMm: request.sensor.heightMm
|
|
227
|
+
},
|
|
228
|
+
nativeRaster: {
|
|
229
|
+
pixelWidth: request.sensor.pixelWidth,
|
|
230
|
+
pixelHeight: request.sensor.pixelHeight
|
|
231
|
+
},
|
|
232
|
+
orientation: capture.orientation,
|
|
233
|
+
...(capture.activeCaptureRect === undefined
|
|
234
|
+
? {}
|
|
235
|
+
: { activeCaptureRect: capture.activeCaptureRect }),
|
|
236
|
+
focalLengthMm: request.lens.focalLengthMm,
|
|
237
|
+
focusDistanceM: request.focus.focusDistanceM
|
|
238
|
+
}).value;
|
|
239
|
+
const equivalentFocalLength = captureGeometry === undefined
|
|
240
|
+
? undefined
|
|
241
|
+
: calculateEquivalentFocalLength35Mm({
|
|
242
|
+
focalLengthMm: request.lens.focalLengthMm,
|
|
243
|
+
activeImagingArea: captureGeometry.activeCapture.imagingArea
|
|
244
|
+
}).value;
|
|
245
|
+
const outputFieldOfView = captureGeometry === undefined || capture === undefined
|
|
246
|
+
? undefined
|
|
247
|
+
: calculateOutputFieldOfView({
|
|
248
|
+
imagingArea: {
|
|
249
|
+
widthMm: request.sensor.widthMm,
|
|
250
|
+
heightMm: request.sensor.heightMm
|
|
251
|
+
},
|
|
252
|
+
nativeRaster: {
|
|
253
|
+
pixelWidth: request.sensor.pixelWidth,
|
|
254
|
+
pixelHeight: request.sensor.pixelHeight
|
|
255
|
+
},
|
|
256
|
+
orientation: capture.orientation,
|
|
257
|
+
...(capture.activeCaptureRect === undefined
|
|
258
|
+
? {}
|
|
259
|
+
: { activeCaptureRect: capture.activeCaptureRect }),
|
|
260
|
+
...(capture.outputCropRect === undefined
|
|
261
|
+
? {}
|
|
262
|
+
: { outputCropRect: capture.outputCropRect }),
|
|
263
|
+
focalLengthMm: request.lens.focalLengthMm,
|
|
264
|
+
focusDistanceM: request.focus.focusDistanceM
|
|
265
|
+
}).value;
|
|
110
266
|
const pixelPitch = calculatePixelPitch({
|
|
111
267
|
sensorWidthMm: request.sensor.widthMm,
|
|
112
268
|
pixelWidth: request.sensor.pixelWidth
|
|
@@ -117,12 +273,6 @@ export function simulatePocCamera(request) {
|
|
|
117
273
|
cropFactor: request.crop.factor
|
|
118
274
|
});
|
|
119
275
|
const cropFieldOfView = calculateCroppedFieldOfView(request.sensor.widthMm, request.sensor.heightMm, request.lens.focalLengthMm, request.focus.focusDistanceM, request.crop.factor);
|
|
120
|
-
const depthOfField = calculateDepthOfField({
|
|
121
|
-
focalLengthMm: request.lens.focalLengthMm,
|
|
122
|
-
aperture: request.lens.aperture,
|
|
123
|
-
focusDistanceM: request.focus.focusDistanceM,
|
|
124
|
-
circleOfConfusionMm
|
|
125
|
-
});
|
|
126
276
|
const diffraction = calculateAiryDisk({
|
|
127
277
|
aperture: request.lens.aperture,
|
|
128
278
|
wavelengthNm: request.diffraction.wavelengthNm
|
|
@@ -149,6 +299,70 @@ export function simulatePocCamera(request) {
|
|
|
149
299
|
focusDistanceM: request.focus.focusDistanceM,
|
|
150
300
|
pixelPitchMicrometers: pixelPitch.value.micrometers
|
|
151
301
|
}).value;
|
|
302
|
+
const captureSubjectFraming = captureGeometry === undefined ||
|
|
303
|
+
capture === undefined ||
|
|
304
|
+
subjectCropRequest === undefined ||
|
|
305
|
+
subjectSampling === undefined
|
|
306
|
+
? undefined
|
|
307
|
+
: (() => {
|
|
308
|
+
const orientedSubjectHeightPixels = isPortraitOrientation(capture.orientation)
|
|
309
|
+
? (subjectSampling.widthPixels ?? 0)
|
|
310
|
+
: (subjectSampling.heightPixels ?? 0);
|
|
311
|
+
const outputSubjectHeightPixels = orientedSubjectHeightPixels *
|
|
312
|
+
captureGeometry.output.orientedCaptureToOutputScale.y;
|
|
313
|
+
const framing = calculateSubjectFramingCrop({
|
|
314
|
+
pixelWidth: captureGeometry.output.raster.pixelWidth,
|
|
315
|
+
pixelHeight: captureGeometry.output.raster.pixelHeight,
|
|
316
|
+
subjectHeightPixels: outputSubjectHeightPixels,
|
|
317
|
+
targetSubjectHeightFraction: subjectCropRequest.targetSubjectHeightFraction
|
|
318
|
+
}).value;
|
|
319
|
+
const retainedBounds = centeredPhysicalCropBounds(captureGeometry.output.physicalBoundsFromOpticalAxisMm, framing.pixelWidth / captureGeometry.output.raster.pixelWidth, framing.pixelHeight / captureGeometry.output.raster.pixelHeight);
|
|
320
|
+
const retainedImagingArea = {
|
|
321
|
+
widthMm: retainedBounds.right - retainedBounds.left,
|
|
322
|
+
heightMm: retainedBounds.bottom - retainedBounds.top
|
|
323
|
+
};
|
|
324
|
+
return {
|
|
325
|
+
additionalCropFactor: framing.cropFactor,
|
|
326
|
+
raster: {
|
|
327
|
+
pixelWidth: framing.pixelWidth,
|
|
328
|
+
pixelHeight: framing.pixelHeight
|
|
329
|
+
},
|
|
330
|
+
megapixels: framing.megapixels,
|
|
331
|
+
subjectHeightFraction: framing.subjectHeightFraction,
|
|
332
|
+
subjectClipped: framing.subjectClipped,
|
|
333
|
+
additionalCropApplied: framing.cropped,
|
|
334
|
+
retainedImagingArea,
|
|
335
|
+
physicalBoundsFromOpticalAxisMm: retainedBounds,
|
|
336
|
+
effectiveFieldOfView: calculateFieldOfViewForPhysicalBounds(retainedBounds, request.lens.focalLengthMm, request.focus.focusDistanceM),
|
|
337
|
+
basis: "centered-output-framing"
|
|
338
|
+
};
|
|
339
|
+
})();
|
|
340
|
+
const equivalentViewingTargetArea = captureGeometry === undefined
|
|
341
|
+
? {
|
|
342
|
+
widthMm: request.sensor.widthMm,
|
|
343
|
+
heightMm: request.sensor.heightMm
|
|
344
|
+
}
|
|
345
|
+
: (captureSubjectFraming?.retainedImagingArea ??
|
|
346
|
+
captureGeometry.output.imagingArea);
|
|
347
|
+
const equivalentViewingCircleOfConfusion = request.focus.equivalentViewingCircleOfConfusion === undefined
|
|
348
|
+
? undefined
|
|
349
|
+
: estimateEquivalentViewingCircleOfConfusion({
|
|
350
|
+
sensorWidthMm: equivalentViewingTargetArea.widthMm,
|
|
351
|
+
sensorHeightMm: equivalentViewingTargetArea.heightMm,
|
|
352
|
+
...request.focus.equivalentViewingCircleOfConfusion
|
|
353
|
+
});
|
|
354
|
+
const circleOfConfusionMm = equivalentViewingCircleOfConfusion?.value.circleOfConfusionMm ??
|
|
355
|
+
request.focus.circleOfConfusionMm;
|
|
356
|
+
if (circleOfConfusionMm === undefined) {
|
|
357
|
+
throw new InvalidScientificInputError("A focus circle-of-confusion criterion is required.");
|
|
358
|
+
}
|
|
359
|
+
requirePositiveFinite("focus.circleOfConfusionMm", circleOfConfusionMm);
|
|
360
|
+
const depthOfField = calculateDepthOfField({
|
|
361
|
+
focalLengthMm: request.lens.focalLengthMm,
|
|
362
|
+
aperture: request.lens.aperture,
|
|
363
|
+
focusDistanceM: request.focus.focusDistanceM,
|
|
364
|
+
circleOfConfusionMm
|
|
365
|
+
});
|
|
152
366
|
const samplingSamples = request.samplingSamples === undefined
|
|
153
367
|
? undefined
|
|
154
368
|
: request.samplingSamples.map((sample) => {
|
|
@@ -191,13 +405,15 @@ export function simulatePocCamera(request) {
|
|
|
191
405
|
(pixelPitch.value.micrometers / 1000)
|
|
192
406
|
};
|
|
193
407
|
});
|
|
194
|
-
const subjectCrop =
|
|
408
|
+
const subjectCrop = capture !== undefined ||
|
|
409
|
+
subjectCropRequest === undefined ||
|
|
410
|
+
subjectSampling === undefined
|
|
195
411
|
? undefined
|
|
196
412
|
: calculateSubjectFramingCrop({
|
|
197
413
|
pixelWidth: crop.value.pixelWidth,
|
|
198
414
|
pixelHeight: crop.value.pixelHeight,
|
|
199
415
|
subjectHeightPixels: subjectSampling.heightPixels ?? 0,
|
|
200
|
-
targetSubjectHeightFraction:
|
|
416
|
+
targetSubjectHeightFraction: subjectCropRequest.targetSubjectHeightFraction
|
|
201
417
|
}).value;
|
|
202
418
|
const totalSubjectCropFactor = subjectCrop === undefined
|
|
203
419
|
? undefined
|
|
@@ -281,8 +497,39 @@ export function simulatePocCamera(request) {
|
|
|
281
497
|
(pixelPitch.value.micrometers / 1000)
|
|
282
498
|
};
|
|
283
499
|
});
|
|
500
|
+
const captureMotion = captureGeometry === undefined || capture === undefined
|
|
501
|
+
? undefined
|
|
502
|
+
: resolveCaptureVector({
|
|
503
|
+
x: motion.value.deltaXMm / (pixelPitch.value.micrometers / 1000),
|
|
504
|
+
y: motion.value.deltaYMm / (pixelPitch.value.micrometers / 1000)
|
|
505
|
+
}, capture.orientation, captureGeometry);
|
|
506
|
+
const captureMotionSamples = captureGeometry === undefined ||
|
|
507
|
+
capture === undefined ||
|
|
508
|
+
motionSamples === undefined
|
|
509
|
+
? undefined
|
|
510
|
+
: motionSamples.map((sample) => ({
|
|
511
|
+
id: sample.id,
|
|
512
|
+
...resolveCaptureVector({
|
|
513
|
+
x: sample.deltaXPixels,
|
|
514
|
+
y: sample.deltaYPixels
|
|
515
|
+
}, capture.orientation, captureGeometry)
|
|
516
|
+
}));
|
|
517
|
+
const captureCameraShake = captureGeometry === undefined ||
|
|
518
|
+
capture === undefined ||
|
|
519
|
+
cameraShake === undefined
|
|
520
|
+
? undefined
|
|
521
|
+
: {
|
|
522
|
+
unstabilized: resolveCaptureVector({
|
|
523
|
+
x: cameraShake.value.unstabilized.deltaXPixels ?? 0,
|
|
524
|
+
y: cameraShake.value.unstabilized.deltaYPixels ?? 0
|
|
525
|
+
}, capture.orientation, captureGeometry),
|
|
526
|
+
stabilized: resolveCaptureVector({
|
|
527
|
+
x: cameraShake.value.stabilized.deltaXPixels ?? 0,
|
|
528
|
+
y: cameraShake.value.stabilized.deltaYPixels ?? 0
|
|
529
|
+
}, capture.orientation, captureGeometry)
|
|
530
|
+
};
|
|
284
531
|
return {
|
|
285
|
-
apiVersion:
|
|
532
|
+
apiVersion: POC_SIMULATION_API_VERSION,
|
|
286
533
|
projection: {
|
|
287
534
|
kind: "focus-aware-thin-lens",
|
|
288
535
|
focusDistanceM: request.focus.focusDistanceM,
|
|
@@ -296,8 +543,39 @@ export function simulatePocCamera(request) {
|
|
|
296
543
|
diagonalDegrees: diagonalFov.value.degrees
|
|
297
544
|
},
|
|
298
545
|
sensor: {
|
|
299
|
-
|
|
546
|
+
geometry: sensorGeometry,
|
|
547
|
+
pixelPitchMicrometers: pixelPitch.value.micrometers,
|
|
548
|
+
pitchXMicrometers,
|
|
549
|
+
pitchYMicrometers,
|
|
550
|
+
pitchAxisRelativeDifference
|
|
300
551
|
},
|
|
552
|
+
...(captureGeometry === undefined ||
|
|
553
|
+
activeCaptureFieldOfView === undefined ||
|
|
554
|
+
outputFieldOfView === undefined ||
|
|
555
|
+
equivalentFocalLength === undefined ||
|
|
556
|
+
captureMotion === undefined
|
|
557
|
+
? {}
|
|
558
|
+
: {
|
|
559
|
+
capture: {
|
|
560
|
+
geometry: captureGeometry,
|
|
561
|
+
activeFieldOfView: activeCaptureFieldOfView,
|
|
562
|
+
outputFieldOfView,
|
|
563
|
+
focalLength: equivalentFocalLength,
|
|
564
|
+
outputSamplingScale: {
|
|
565
|
+
...captureGeometry.output.orientedCaptureToOutputScale
|
|
566
|
+
},
|
|
567
|
+
...(captureSubjectFraming === undefined
|
|
568
|
+
? {}
|
|
569
|
+
: { subjectFraming: captureSubjectFraming }),
|
|
570
|
+
motion: captureMotion,
|
|
571
|
+
...(captureMotionSamples === undefined
|
|
572
|
+
? {}
|
|
573
|
+
: { motionSamples: captureMotionSamples }),
|
|
574
|
+
...(captureCameraShake === undefined
|
|
575
|
+
? {}
|
|
576
|
+
: { cameraShake: captureCameraShake })
|
|
577
|
+
}
|
|
578
|
+
}),
|
|
301
579
|
crop: {
|
|
302
580
|
cropFactor: request.crop.factor,
|
|
303
581
|
...crop.value,
|
|
@@ -312,6 +590,12 @@ export function simulatePocCamera(request) {
|
|
|
312
590
|
source: "equivalent-viewing-approximation",
|
|
313
591
|
circleOfConfusionMm,
|
|
314
592
|
scaleFactor: equivalentViewingCircleOfConfusion.value.scaleFactor,
|
|
593
|
+
targetBasis: captureGeometry === undefined
|
|
594
|
+
? "full-sensor"
|
|
595
|
+
: "final-retained-output",
|
|
596
|
+
targetImagingArea: {
|
|
597
|
+
...equivalentViewingTargetArea
|
|
598
|
+
},
|
|
315
599
|
provenance: {
|
|
316
600
|
kind: "approximation",
|
|
317
601
|
model: equivalentViewingCircleOfConfusion.provenance.model,
|