@photivra/engine 0.2.0 → 0.5.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 +79 -0
- package/CITATION.cff +1 -1
- package/README.md +59 -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 +117 -0
- package/dist/optics/lateral-chromatic-aberration.d.ts.map +1 -0
- package/dist/optics/lateral-chromatic-aberration.js +228 -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 +131 -0
- package/dist/optics/radial-distortion.d.ts.map +1 -0
- package/dist/optics/radial-distortion.js +227 -0
- package/dist/optics/radial-distortion.js.map +1 -0
- package/dist/output/capture-geometry.d.ts +278 -0
- package/dist/output/capture-geometry.d.ts.map +1 -0
- package/dist/output/capture-geometry.js +562 -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 +36 -2
- package/docs/ARCHITECTURE.md +179 -1
- package/docs/IMAGE_FORMATION.md +151 -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 +714 -3
- package/package.json +3 -2
package/docs/USAGE.md
CHANGED
|
@@ -14,6 +14,30 @@ console.log(result.quality);
|
|
|
14
14
|
|
|
15
15
|
`quality` is optional and is omitted when the engine does not have defensible quantified uncertainty/accuracy metadata to report.
|
|
16
16
|
|
|
17
|
+
## Image-formation contract
|
|
18
|
+
|
|
19
|
+
Use the public image-formation contract when coordinating effects that cross optics, motion, sensor, and output domains:
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import { getImageFormationContract } from "@photivra/engine";
|
|
23
|
+
|
|
24
|
+
const contract = getImageFormationContract();
|
|
25
|
+
|
|
26
|
+
console.log(contract.domains);
|
|
27
|
+
console.log(contract.temporal.authoritativeTimeUnit);
|
|
28
|
+
console.log(
|
|
29
|
+
contract.effectPlacements.find(
|
|
30
|
+
(effect) => effect.id === "mechanical-vignetting"
|
|
31
|
+
)
|
|
32
|
+
);
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The contract is descriptive metadata. It does not imply that every reserved stage is implemented, and it does not change the composed POC request/response contract.
|
|
36
|
+
|
|
37
|
+
Use `requiredUpstreamStages` for hard scientific dependencies and `coupledStages` for shared state/interactions that must not be treated as independent renderer filters.
|
|
38
|
+
|
|
39
|
+
See [Image-Formation Contract](IMAGE_FORMATION.md) for coordinate, temporal, renderer, and reserved sensor-stage semantics.
|
|
40
|
+
|
|
17
41
|
## Field of view
|
|
18
42
|
|
|
19
43
|
Use `calculateFieldOfView()` for one sensor dimension at a time.
|
|
@@ -44,8 +68,317 @@ console.log(focused.value.degrees);
|
|
|
44
68
|
|
|
45
69
|
Without `focusDistanceM`, nominal focal length is used as the infinity-focus/pinhole projection distance. Neither mode models real-lens distortion or focus breathing.
|
|
46
70
|
|
|
71
|
+
For off-center sensor regions, use `calculateFieldOfViewBounds()` with signed sensor-plane coordinates relative to the optical axis:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { calculateFieldOfViewBounds } from "@photivra/engine";
|
|
75
|
+
|
|
76
|
+
const leftHalf = calculateFieldOfViewBounds({
|
|
77
|
+
focalLengthMm: 50,
|
|
78
|
+
minimumSensorCoordinateMm: -18,
|
|
79
|
+
maximumSensorCoordinateMm: 0
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
console.log(leftHalf.value.minimumDegrees);
|
|
83
|
+
console.log(leftHalf.value.maximumDegrees);
|
|
84
|
+
console.log(leftHalf.value.degrees);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
Unlike the centered `2 × atan(size / 2d)` form, the bounds API preserves asymmetric angular limits and is used by active-capture geometry for off-center crops.
|
|
88
|
+
|
|
47
89
|
See [Physics Foundation](PHYSICS_FOUNDATION.md#rectilinear-field-of-view).
|
|
48
90
|
|
|
91
|
+
## Actual and 35 mm-equivalent focal length
|
|
92
|
+
|
|
93
|
+
Use `calculateEquivalentFocalLength35Mm()` when you need the conventional diagonal-based 35 mm-equivalent value while preserving the physical optical focal length:
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { calculateEquivalentFocalLength35Mm } from "@photivra/engine";
|
|
97
|
+
|
|
98
|
+
const equivalent = calculateEquivalentFocalLength35Mm({
|
|
99
|
+
focalLengthMm: 4.5,
|
|
100
|
+
activeImagingArea: {
|
|
101
|
+
widthMm: 6.17,
|
|
102
|
+
heightMm: 4.55
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
console.log(equivalent.value.actualFocalLengthMm); // 4.5
|
|
107
|
+
console.log(equivalent.value.equivalentFocalLength35Mm);
|
|
108
|
+
console.log(equivalent.value.basis); // "diagonal"
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
The actual focal length remains the physical optical quantity used by projection and depth-of-field calculations. The equivalent value is derived from the active physical capture diagonal relative to a 36 × 24 mm reference frame.
|
|
112
|
+
|
|
113
|
+
If an active sensor crop is in use, pass the physical active area resolved by `resolveCaptureGeometry()`. Physical camera orientation does not change the equivalent value because the active diagonal is unchanged.
|
|
114
|
+
|
|
115
|
+
Focus distance is not part of conventional 35 mm-equivalent focal length. Later digital/output crop and output resolution are also excluded. If a product wants to describe final digital framing, it should label that as output framing rather than silently replacing the optical focal length or capture-equivalent focal length.
|
|
116
|
+
|
|
117
|
+
`calculateImagingAreaMetrics()` exposes the same canonical diagonal crop-factor calculation independently of raster density so other engine modules do not need to duplicate that formula.
|
|
118
|
+
|
|
119
|
+
## Focus-breathing projection
|
|
120
|
+
|
|
121
|
+
Use `calculateFocusBreathingProjection()` when a caller has an explicit projection-scale value for the selected focus state.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
import {
|
|
125
|
+
calculateFocusBreathingFieldOfView,
|
|
126
|
+
calculateFocusBreathingProjection
|
|
127
|
+
} from "@photivra/engine";
|
|
128
|
+
|
|
129
|
+
const projection = calculateFocusBreathingProjection({
|
|
130
|
+
focalLengthMm: 50,
|
|
131
|
+
focusDistanceM: 1.5,
|
|
132
|
+
breathingProjectionScale: 1.04
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const horizontalFov = calculateFocusBreathingFieldOfView({
|
|
136
|
+
focalLengthMm: 50,
|
|
137
|
+
focusDistanceM: 1.5,
|
|
138
|
+
sensorDimensionMm: 36,
|
|
139
|
+
breathingProjectionScale: 1.04
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
console.log(projection.value.physicalFocalLengthMm); // still 50
|
|
143
|
+
console.log(projection.value.effectiveProjectionDistanceMm);
|
|
144
|
+
console.log(horizontalFov.value.effectiveDegrees);
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
`breathingProjectionScale` is dimensionless and applies to the ideal thin-lens projection distance for that one focus state:
|
|
148
|
+
|
|
149
|
+
- `1` means no additional breathing relative to the current thin-lens model;
|
|
150
|
+
- values above `1` narrow framing / increase magnification;
|
|
151
|
+
- values below `1` widen framing / reduce magnification.
|
|
152
|
+
|
|
153
|
+
Photivra does **not** infer this scale from focal length, focus distance, lens identity, sensor format, or marketing specifications. Supplying the same scale at two focus distances means exactly that: the caller explicitly declared the same relative projection scale at both states.
|
|
154
|
+
|
|
155
|
+
The physical focal length remains authoritative. The effective projection distance is not a renamed focal length and later digital/output crop does not redefine the breathing scale.
|
|
156
|
+
|
|
157
|
+
This generic model is returned as an `approximation`. A future calibrated lens profile would need defensible provenance, reuse rights, and uncertainty/limitation metadata.
|
|
158
|
+
|
|
159
|
+
## Oriented raster to image-plane coordinates
|
|
160
|
+
|
|
161
|
+
Use the coordinate bridge before calling image-plane lens or camera-mapping APIs
|
|
162
|
+
from an oriented/output renderer. The capture/output physical region uses the
|
|
163
|
+
raster-style +Y-down basis, while lens-field APIs use the pre-orientation
|
|
164
|
+
optical image plane with +Y up.
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
import {
|
|
168
|
+
mapImagePlanePointToOrientedPhysicalUv,
|
|
169
|
+
mapOrientedPhysicalUvToImagePlanePoint
|
|
170
|
+
} from "@photivra/engine";
|
|
171
|
+
|
|
172
|
+
const bounds = { left: -12, right: 12, top: -18, bottom: 18 };
|
|
173
|
+
|
|
174
|
+
const destination = mapOrientedPhysicalUvToImagePlanePoint({
|
|
175
|
+
uv: { u: 0.25, v: 0.75 },
|
|
176
|
+
orientedPhysicalBoundsFromOpticalAxisMm: bounds,
|
|
177
|
+
orientation: "portrait-clockwise"
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const roundTrip = mapImagePlanePointToOrientedPhysicalUv({
|
|
181
|
+
imagePlanePointMm: destination,
|
|
182
|
+
orientedPhysicalBoundsFromOpticalAxisMm: bounds,
|
|
183
|
+
orientation: "portrait-clockwise"
|
|
184
|
+
});
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
These functions only transform coordinate bases. They do not apply projection,
|
|
188
|
+
distortion, focus breathing, crop, or resampling. Asymmetric/off-axis physical
|
|
189
|
+
bounds are preserved rather than recentered, and reverse mapping fails closed
|
|
190
|
+
when the image-plane point lies outside the supplied physical region.
|
|
191
|
+
|
|
192
|
+
## Radial lens-distortion mapping
|
|
193
|
+
|
|
194
|
+
Use `calculateRadialDistortionMapping()` for a generic rotationally symmetric ideal-to-distorted image-plane mapping and `calculateInverseRadialDistortionMapping()` for the destination-to-source mapping a renderer needs for inverse sampling.
|
|
195
|
+
|
|
196
|
+
```ts
|
|
197
|
+
import {
|
|
198
|
+
calculateInverseRadialDistortionMapping,
|
|
199
|
+
calculateRadialDistortionMapping
|
|
200
|
+
} from "@photivra/engine";
|
|
201
|
+
|
|
202
|
+
const profile = {
|
|
203
|
+
normalizationRadiusMm: 21.63,
|
|
204
|
+
maximumNormalizedRadius: 1,
|
|
205
|
+
coefficients: {
|
|
206
|
+
k1: -0.08,
|
|
207
|
+
k2: 0.025,
|
|
208
|
+
k3: -0.004
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const forward = calculateRadialDistortionMapping({
|
|
213
|
+
imagePointMm: { x: 14, y: 8 },
|
|
214
|
+
profile
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const inverse = calculateInverseRadialDistortionMapping({
|
|
218
|
+
distortedImagePointMm: forward.value.mappedImagePointMm,
|
|
219
|
+
profile
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
console.log(forward.value.radialScale);
|
|
223
|
+
console.log(inverse.value.sourceImagePointMm);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
The model is:
|
|
227
|
+
|
|
228
|
+
```text
|
|
229
|
+
r = image-plane radius / normalizationRadiusMm
|
|
230
|
+
scale = 1 + k1 r² + k2 r⁴ + k3 r⁶
|
|
231
|
+
p_distorted = p_ideal × scale
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
The coefficients are dimensionless **only together with the declared physical normalization radius**. Reusing coefficients with a different normalization changes the model.
|
|
235
|
+
|
|
236
|
+
`maximumNormalizedRadius` is the caller-declared valid operating envelope. Photivra analytically checks that radial distance remains strictly monotonic over that interval; profiles that fold over are rejected so inverse mapping is unique.
|
|
237
|
+
|
|
238
|
+
This first field-mapping slice is radial-only and centered on the optical axis. It does not model tangential/decentered distortion, anamorphic mapping, wavelength dependence, or a calibrated named lens.
|
|
239
|
+
|
|
240
|
+
The engine returns an `approximation` because the polynomial is a generic caller-parameterized lens model. Test Fixture grid/fiducials can provide renderer regression evidence, but they do not calibrate real-lens coefficients.
|
|
241
|
+
|
|
242
|
+
## Lateral chromatic-aberration mapping
|
|
243
|
+
|
|
244
|
+
Use `calculateLateralChromaticAberrationMapping()` to map one ideal image-plane point through a shared **green-reference base distortion** plus red/blue radial coefficient offsets.
|
|
245
|
+
|
|
246
|
+
Use `calculateInverseLateralChromaticAberrationMapping()` when a renderer needs the ideal source coordinate for each output channel at one distorted destination.
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import {
|
|
250
|
+
calculateInverseLateralChromaticAberrationMapping,
|
|
251
|
+
calculateLateralChromaticAberrationMapping
|
|
252
|
+
} from "@photivra/engine";
|
|
253
|
+
|
|
254
|
+
const profile = {
|
|
255
|
+
normalizationRadiusMm: 21.63,
|
|
256
|
+
maximumNormalizedRadius: 1,
|
|
257
|
+
baseDistortionCoefficients: {
|
|
258
|
+
k1: -0.06,
|
|
259
|
+
k2: 0.018,
|
|
260
|
+
k3: 0
|
|
261
|
+
},
|
|
262
|
+
redCoefficientOffset: {
|
|
263
|
+
k1: 0.02,
|
|
264
|
+
k2: -0.006,
|
|
265
|
+
k3: 0
|
|
266
|
+
},
|
|
267
|
+
blueCoefficientOffset: {
|
|
268
|
+
k1: -0.02,
|
|
269
|
+
k2: 0.006,
|
|
270
|
+
k3: 0
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const forward = calculateLateralChromaticAberrationMapping({
|
|
275
|
+
imagePointMm: { x: 14, y: 8 },
|
|
276
|
+
profile
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
const inverse = calculateInverseLateralChromaticAberrationMapping({
|
|
280
|
+
distortedImagePointMm: { x: 14, y: 8 },
|
|
281
|
+
profile
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
console.log(forward.value.separation);
|
|
285
|
+
console.log(inverse.value.channels.red.sourceImagePointMm);
|
|
286
|
+
console.log(inverse.value.channels.green.sourceImagePointMm);
|
|
287
|
+
console.log(inverse.value.channels.blue.sourceImagePointMm);
|
|
288
|
+
```
|
|
289
|
+
|
|
290
|
+
This is **field mapping, not a blur kernel**.
|
|
291
|
+
|
|
292
|
+
The green channel carries the common base geometric distortion. Red and blue offsets are added to that base before mapping. Therefore:
|
|
293
|
+
|
|
294
|
+
- zero red/blue offsets mean no lateral CA, while base distortion can still exist;
|
|
295
|
+
- callers should not apply the same base distortion as a second renderer warp;
|
|
296
|
+
- all three combined channel profiles share one physical normalization radius and operating envelope;
|
|
297
|
+
- every combined channel profile must remain individually invertible over that envelope.
|
|
298
|
+
|
|
299
|
+
The channel labels are representative renderer RGB channels. They are not wavelength samples, a spectral lens model, sensor CFA primaries, or a camera colorimetric profile. Longitudinal chromatic aberration and wavelength-dependent PSF behavior remain separate future work.
|
|
300
|
+
|
|
301
|
+
A renderer should inverse-map each destination channel to the source coordinate returned by the engine instead of adding a backend-specific RGB offset or finished-image fringe blur.
|
|
302
|
+
|
|
303
|
+
For renderer grids or other multi-point evaluation, use the batch inverse APIs so
|
|
304
|
+
one profile validation/provenance boundary serves the full point set:
|
|
305
|
+
|
|
306
|
+
```ts
|
|
307
|
+
import {
|
|
308
|
+
calculateInverseLateralChromaticAberrationMappings,
|
|
309
|
+
calculateInverseRadialDistortionMappings
|
|
310
|
+
} from "@photivra/engine";
|
|
311
|
+
|
|
312
|
+
const radialGrid = calculateInverseRadialDistortionMappings({
|
|
313
|
+
distortedImagePointsMm: points,
|
|
314
|
+
profile: radialProfile
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
const caGrid = calculateInverseLateralChromaticAberrationMappings({
|
|
318
|
+
distortedImagePointsMm: points,
|
|
319
|
+
profile: caProfile
|
|
320
|
+
});
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
Batch per-point values are defined to match the corresponding scalar inverse
|
|
324
|
+
APIs. Batching changes repeated validation/allocation structure, not the lens
|
|
325
|
+
model, operating envelope, coordinate basis, or numerical solver.
|
|
326
|
+
|
|
327
|
+
## Illumination vignetting
|
|
328
|
+
|
|
329
|
+
Use `calculateIlluminationVignetting()` when a generic virtual lens needs field-dependent **relative linear throughput** without changing geometry or pupil/PSF shape.
|
|
330
|
+
|
|
331
|
+
```ts
|
|
332
|
+
import { calculateIlluminationVignetting } from "@photivra/engine";
|
|
333
|
+
|
|
334
|
+
const illumination = calculateIlluminationVignetting({
|
|
335
|
+
imagePointMm: { x: 16, y: 0 },
|
|
336
|
+
profile: {
|
|
337
|
+
normalizationRadiusMm: 20,
|
|
338
|
+
maximumNormalizedRadius: 1,
|
|
339
|
+
coefficients: {
|
|
340
|
+
r2: -0.5,
|
|
341
|
+
r4: 0.1,
|
|
342
|
+
r6: 0
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
console.log(illumination.value.linearThroughputFactor);
|
|
348
|
+
console.log(illumination.value.attenuationStops);
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
The generic radial model is:
|
|
352
|
+
|
|
353
|
+
```text
|
|
354
|
+
rho = image-plane radius / normalizationRadiusMm
|
|
355
|
+
T(rho) = 1 + r2*rho² + r4*rho⁴ + r6*rho⁶
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
`T` is a multiplicative **scene-linear/channel-linear** throughput factor. Apply it before display transfer/gamma encoding.
|
|
359
|
+
|
|
360
|
+
The profile declares `maximumNormalizedRadius`. Photivra analytically checks the complete interval, including internal extrema, and rejects profiles that:
|
|
361
|
+
|
|
362
|
+
- reach zero or negative throughput; or
|
|
363
|
+
- amplify above the optical-axis normalization of `1`.
|
|
364
|
+
|
|
365
|
+
The output also reports positive attenuation in stops:
|
|
366
|
+
|
|
367
|
+
```text
|
|
368
|
+
attenuationStops = -log2(T)
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
This model changes throughput only. It does not change:
|
|
372
|
+
|
|
373
|
+
- image-plane coordinates;
|
|
374
|
+
- focus;
|
|
375
|
+
- pupil shape;
|
|
376
|
+
- PSF shape;
|
|
377
|
+
- bokeh;
|
|
378
|
+
- channel geometry.
|
|
379
|
+
|
|
380
|
+
Mechanical/pupil vignetting and cat's-eye bokeh belong to the later pupil/PSF foundation. This generic profile is also not calibrated radiometry or a named-lens measurement.
|
|
381
|
+
|
|
49
382
|
## Thin-lens image distance and magnification
|
|
50
383
|
|
|
51
384
|
Use `calculateThinLensImageDistance()` to calculate ideal Gaussian thin-lens image distance for an object/focus plane.
|
|
@@ -133,6 +466,45 @@ The circle-of-confusion criterion used for depth-of-field is caller supplied. Th
|
|
|
133
466
|
|
|
134
467
|
See [Physics Foundation](PHYSICS_FOUNDATION.md#thin-lens-focus-and-depth-of-field).
|
|
135
468
|
|
|
469
|
+
## PSF and pupil foundation
|
|
470
|
+
|
|
471
|
+
Use `getPsfFoundationContract()` to inspect current/reserved PSF contribution ownership and `calculatePsfFoundationComponents()` to evaluate the currently implemented diagnostics in one explicit field/depth/spectral/pupil context.
|
|
472
|
+
|
|
473
|
+
```ts
|
|
474
|
+
import {
|
|
475
|
+
calculatePsfFoundationComponents,
|
|
476
|
+
getPsfFoundationContract
|
|
477
|
+
} from "@photivra/engine";
|
|
478
|
+
|
|
479
|
+
const contract = getPsfFoundationContract();
|
|
480
|
+
|
|
481
|
+
const components = calculatePsfFoundationComponents({
|
|
482
|
+
focalLengthMm: 85,
|
|
483
|
+
aperture: 2.8,
|
|
484
|
+
focusDistanceM: 10,
|
|
485
|
+
subjectDistanceM: 20,
|
|
486
|
+
fieldPointMm: { x: 12, y: 8 },
|
|
487
|
+
fieldNormalizationRadiusMm: 21.6,
|
|
488
|
+
spectralBasis: {
|
|
489
|
+
kind: "monochromatic",
|
|
490
|
+
wavelengthNm: 550
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
console.log(contract.compositionPolicy);
|
|
495
|
+
console.log(components.value.contributions.geometricDefocus);
|
|
496
|
+
console.log(components.value.contributions.circularDiffraction);
|
|
497
|
+
console.log(components.value.composition.status); // "not-composed"
|
|
498
|
+
```
|
|
499
|
+
|
|
500
|
+
The current foundation intentionally **does not combine** geometric defocus and circular diffraction into a synthetic PSF, MTF, convolution kernel, combined blur radius, or “lens sharpness” score.
|
|
501
|
+
|
|
502
|
+
It records field position now even though the current defocus/Airy diagnostics are field invariant. Future field-curvature, pupil-clipping, bokeh, and aberration contributions can therefore consume the same context without reinterpreting existing outputs.
|
|
503
|
+
|
|
504
|
+
The current spectral basis is explicitly monochromatic for the Airy diagnostic. This does not create a spectral lens or sensor-color model.
|
|
505
|
+
|
|
506
|
+
See [PSF and Pupil Foundation](PSF_FOUNDATION.md).
|
|
507
|
+
|
|
136
508
|
## Circular-aperture diffraction
|
|
137
509
|
|
|
138
510
|
Use `calculateAiryDisk()` for the ideal monochromatic circular-pupil first-zero Airy diameter:
|
|
@@ -191,7 +563,165 @@ console.log(pitch.value.millimeters);
|
|
|
191
563
|
|
|
192
564
|
The current primitive calculates horizontal pixel pitch. It assumes the supplied sensor width and pixel count describe the same active dimension.
|
|
193
565
|
|
|
194
|
-
See [Physics Foundation](PHYSICS_FOUNDATION.md#
|
|
566
|
+
See [Physics Foundation](PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
|
|
567
|
+
|
|
568
|
+
## Sensor imaging area and native raster
|
|
569
|
+
|
|
570
|
+
Use `calculateSensorGeometryMetrics()` when physical imaging size and native image resolution need to remain independent:
|
|
571
|
+
|
|
572
|
+
```ts
|
|
573
|
+
import { calculateSensorGeometryMetrics } from "@photivra/engine";
|
|
574
|
+
|
|
575
|
+
const metrics = calculateSensorGeometryMetrics({
|
|
576
|
+
imagingArea: {
|
|
577
|
+
widthMm: 36,
|
|
578
|
+
heightMm: 24
|
|
579
|
+
},
|
|
580
|
+
nativeRaster: {
|
|
581
|
+
pixelWidth: 6000,
|
|
582
|
+
pixelHeight: 4000
|
|
583
|
+
}
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
console.log(metrics.value.imagingArea.cropFactor35Mm); // 1
|
|
587
|
+
console.log(metrics.value.nativeRaster.megapixels); // 24
|
|
588
|
+
console.log(metrics.value.sampling.pitchXMicrometers); // 6
|
|
589
|
+
console.log(metrics.value.sampling.pitchYMicrometers); // 6
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
`SensorImagingArea` means the physical photosensitive imaging area used for image formation, not sensor package or die dimensions. `NativeImageRaster` is the effective native image-sampling grid; it does not assert a one-to-one relationship between image samples and physical photodiodes/photosites.
|
|
593
|
+
|
|
594
|
+
The 35 mm crop factor is diagonal-based relative to a 36 × 24 mm reference frame. Changing native raster density does not change the physical crop factor. Changing physical imaging area does not inherently change native megapixels.
|
|
595
|
+
|
|
596
|
+
The returned X/Y sampling pitches are geometric image-sample spacing. They are not photodiode active area, fill factor, or a photon-collection model and must not be used as those quantities.
|
|
597
|
+
|
|
598
|
+
The existing `calculatePixelPitch()` API remains available for callers that only need horizontal pitch from sensor width and horizontal pixel count.
|
|
599
|
+
|
|
600
|
+
## Sensor architecture metadata
|
|
601
|
+
|
|
602
|
+
Use `parseSensorArchitectureProfile()` for descriptive hardware/capability metadata that crosses an untrusted JSON boundary:
|
|
603
|
+
|
|
604
|
+
```ts
|
|
605
|
+
import { parseSensorArchitectureProfile } from "@photivra/engine";
|
|
606
|
+
|
|
607
|
+
const architecture = parseSensorArchitectureProfile({
|
|
608
|
+
schemaVersion: "0.2.0",
|
|
609
|
+
illumination: {
|
|
610
|
+
value: "bsi",
|
|
611
|
+
evidence: [
|
|
612
|
+
{
|
|
613
|
+
sourceOrigin: "manufacturer",
|
|
614
|
+
sourceReference: "manufacturer-spec:example",
|
|
615
|
+
reuseStatus: "factual-reference-only"
|
|
616
|
+
}
|
|
617
|
+
]
|
|
618
|
+
},
|
|
619
|
+
integration: {
|
|
620
|
+
value: "stacked",
|
|
621
|
+
evidence: [
|
|
622
|
+
{
|
|
623
|
+
sourceOrigin: "manufacturer",
|
|
624
|
+
sourceReference: "manufacturer-spec:example",
|
|
625
|
+
reuseStatus: "factual-reference-only"
|
|
626
|
+
}
|
|
627
|
+
]
|
|
628
|
+
},
|
|
629
|
+
readoutCapabilities: [
|
|
630
|
+
{
|
|
631
|
+
value: "rolling",
|
|
632
|
+
evidence: [
|
|
633
|
+
{
|
|
634
|
+
sourceOrigin: "manufacturer",
|
|
635
|
+
sourceReference: "manufacturer-spec:example",
|
|
636
|
+
reuseStatus: "factual-reference-only"
|
|
637
|
+
}
|
|
638
|
+
]
|
|
639
|
+
}
|
|
640
|
+
],
|
|
641
|
+
colorSamplingFamily: {
|
|
642
|
+
value: "bayer",
|
|
643
|
+
evidence: [
|
|
644
|
+
{
|
|
645
|
+
sourceOrigin: "manufacturer",
|
|
646
|
+
sourceReference: "manufacturer-spec:example",
|
|
647
|
+
reuseStatus: "factual-reference-only"
|
|
648
|
+
}
|
|
649
|
+
]
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
The axes are independent. BSI may be stacked or monolithic; stacking does not imply global shutter; and global readout capability does not imply a particular stacking architecture.
|
|
655
|
+
|
|
656
|
+
Source origin and reuse rights are also independent. Manufacturer or third-party material may be factual-reference-only or explicitly reusable when an appropriate license is present. `photivra-owned` evidence must originate from Photivra. Reusable-data evidence requires an explicit license.
|
|
657
|
+
|
|
658
|
+
Scalar facts may cite multiple evidence records. Multi-valued capabilities such as rolling/global readout carry evidence independently for each value so one source does not silently support another capability.
|
|
659
|
+
|
|
660
|
+
`readoutCapabilities` describes hardware capabilities, not the mode selected for one exposure. Capture-specific readout selection and timing belong to later readout/capture-mode models.
|
|
661
|
+
|
|
662
|
+
Unknown facts should be omitted instead of inferred. Architecture metadata remains descriptive only: BSI, stacking, readout family, and CFA family do not directly change FOV, crop factor, pixel pitch, exposure, noise, or dynamic range. A separate documented downstream physical/calibration model is required before any such effect can be claimed.
|
|
663
|
+
|
|
664
|
+
## Capture orientation, active area, and output geometry
|
|
665
|
+
|
|
666
|
+
Use `resolveCaptureGeometry()` to keep physical sensor identity, active capture, physical camera orientation, and final digital output geometry separate:
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { resolveCaptureGeometry } from "@photivra/engine";
|
|
670
|
+
|
|
671
|
+
const geometry = resolveCaptureGeometry({
|
|
672
|
+
imagingArea: { widthMm: 36, heightMm: 24 },
|
|
673
|
+
nativeRaster: { pixelWidth: 6000, pixelHeight: 4000 },
|
|
674
|
+
orientation: "portrait-clockwise",
|
|
675
|
+
activeCaptureRect: {
|
|
676
|
+
x: 1000,
|
|
677
|
+
y: 800,
|
|
678
|
+
width: 4000,
|
|
679
|
+
height: 2400
|
|
680
|
+
},
|
|
681
|
+
outputCropRect: {
|
|
682
|
+
x: 0,
|
|
683
|
+
y: 875,
|
|
684
|
+
width: 2400,
|
|
685
|
+
height: 2250
|
|
686
|
+
},
|
|
687
|
+
outputRaster: {
|
|
688
|
+
pixelWidth: 2160,
|
|
689
|
+
pixelHeight: 2025
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
console.log(geometry.value.activeCapture.centerOffsetFromOpticalAxisMm);
|
|
694
|
+
console.log(geometry.value.orientedCapture.raster);
|
|
695
|
+
console.log(geometry.value.output.raster);
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
Native sensor raster coordinates are invariant under physical camera rotation:
|
|
699
|
+
|
|
700
|
+
- origin: top-left;
|
|
701
|
+
- +X: right;
|
|
702
|
+
- +Y: down;
|
|
703
|
+
- rectangles: integer, half-open extents `[x, x + width) × [y, y + height)`.
|
|
704
|
+
|
|
705
|
+
The engine exports exact native↔oriented point, vector, and rectangle transforms for all four physical rotations:
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
import {
|
|
709
|
+
transformNativeRasterPointToOriented,
|
|
710
|
+
transformOrientedRasterPointToNative
|
|
711
|
+
} from "@photivra/engine";
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
Point transforms use continuous raster-edge coordinates; pixel centers may be represented with +0.5 offsets. Vector transforms rotate direction only. Rectangle transforms preserve integer half-open semantics. Clockwise and counter-clockwise portrait transforms are distinct and round-trip to native coordinates.
|
|
715
|
+
|
|
716
|
+
`activeCaptureRect` is expressed in native coordinates. The generic model derives physical active-area bounds by assuming native image samples uniformly span the declared physical imaging area. The result exposes physical bounds and center offset relative to the optical axis so off-center crops remain optically asymmetric.
|
|
717
|
+
|
|
718
|
+
`outputCropRect` is expressed in oriented active-capture coordinates. `outputRaster` may resample that crop but must preserve its aspect ratio within integer-rounding tolerance; implicit geometric stretching is rejected.
|
|
719
|
+
|
|
720
|
+
Display/file transforms such as EXIF mirroring remain separate from physical `CaptureOrientation`.
|
|
721
|
+
|
|
722
|
+
Use `calculateActiveCaptureFieldOfView()` for active physical capture FOV. It preserves signed horizontal/vertical bounds for off-center crops and reports both opposite-corner diagonal spans. `diagonalDegrees` is the larger of those spans; centered crops produce equal diagonal spans.
|
|
723
|
+
|
|
724
|
+
Final digital/output crop is deliberately excluded from active-capture FOV.
|
|
195
725
|
|
|
196
726
|
## Centered crop and subject framing crop
|
|
197
727
|
|
|
@@ -231,7 +761,7 @@ console.log(framing.value.subjectClipped);
|
|
|
231
761
|
|
|
232
762
|
Subject framing assumes the crop can be positioned around the subject; it does not check the subject's actual position against image edges.
|
|
233
763
|
|
|
234
|
-
See [Physics Foundation](PHYSICS_FOUNDATION.md#
|
|
764
|
+
See [Physics Foundation](PHYSICS_FOUNDATION.md#sensor-geometry-sampling-capture-and-crop).
|
|
235
765
|
|
|
236
766
|
## Projected subject motion
|
|
237
767
|
|
|
@@ -332,6 +862,47 @@ These relations do not model scene radiance, lens T-stop/transmission, vignettin
|
|
|
332
862
|
|
|
333
863
|
See [Motion and Signal Foundation](MOTION_AND_SIGNAL.md#exposure-relations).
|
|
334
864
|
|
|
865
|
+
## Spatial camera-rotation mapping
|
|
866
|
+
|
|
867
|
+
Use `calculateCameraRotationImageMapping()` when you need the image-plane location of a stationary world ray after **pure camera rotation** at an arbitrary physical time from exposure start.
|
|
868
|
+
|
|
869
|
+
```ts
|
|
870
|
+
import { calculateCameraRotationImageMapping } from "@photivra/engine";
|
|
871
|
+
|
|
872
|
+
const mapped = calculateCameraRotationImageMapping({
|
|
873
|
+
focalLengthMm: 50,
|
|
874
|
+
imagePointMm: { x: 18, y: 12 },
|
|
875
|
+
timeSecondsFromExposureStart: 1 / 60,
|
|
876
|
+
angularVelocityRadPerSec: {
|
|
877
|
+
pitch: 0.01,
|
|
878
|
+
yaw: 0.02,
|
|
879
|
+
roll: 0.005
|
|
880
|
+
},
|
|
881
|
+
samplingPitchMicrometers: {
|
|
882
|
+
x: 6,
|
|
883
|
+
y: 6
|
|
884
|
+
}
|
|
885
|
+
});
|
|
886
|
+
|
|
887
|
+
console.log(mapped.value.mappedImagePointMm);
|
|
888
|
+
console.log(mapped.value.deltaMm);
|
|
889
|
+
console.log(mapped.value.deltaImagePlaneSamples);
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Coordinate/sign convention:
|
|
893
|
+
|
|
894
|
+
- camera axes at exposure start are +X right, +Y up, +Z forward;
|
|
895
|
+
- positive pitch/yaw/roll follow the right-hand rule about +X/+Y/+Z;
|
|
896
|
+
- the returned image-plane basis is +X right, +Y up;
|
|
897
|
+
- a stationary world ray is transformed by the **inverse** camera rotation, so positive physical camera rotation generally moves scene imagery in the opposite screen direction;
|
|
898
|
+
- optional `deltaImagePlaneSamples` retains image-plane +Y-up semantics and is **not** a native-raster (+Y-down) vector.
|
|
899
|
+
|
|
900
|
+
The angular-velocity vector is assumed constant and is integrated as one axis-angle rotation. This avoids arbitrary Euler ordering for simultaneous pitch/yaw/roll.
|
|
901
|
+
|
|
902
|
+
The function is intentionally rotation-only. Camera translation is excluded because translational optical flow depends on scene depth/parallax.
|
|
903
|
+
|
|
904
|
+
It is also separate from stabilization. `estimateCameraShakeBlur()` remains the older educational stabilization-equivalent approximation and keeps its existing global-vector semantics for compatibility.
|
|
905
|
+
|
|
335
906
|
## Camera shake and stabilization-equivalent approximation
|
|
336
907
|
|
|
337
908
|
Use `estimateCameraShakeBlur()` with a controlled yaw/pitch angular-velocity profile:
|
|
@@ -361,6 +932,74 @@ The `2^-stops` attenuation is an educational approximation, not a real IBIS/OIS
|
|
|
361
932
|
|
|
362
933
|
See [Camera Shake and Stabilization](STABILIZATION.md).
|
|
363
934
|
|
|
935
|
+
## Radiometry readiness
|
|
936
|
+
|
|
937
|
+
Use `parseRadiometryReadinessProfile()` and `assessRadiometryReadiness()` to validate whether a declared radiometric calibration package contains the prerequisites needed for a future photon estimate.
|
|
938
|
+
|
|
939
|
+
The gate covers six independently declared requirements:
|
|
940
|
+
|
|
941
|
+
1. scene spectral radiance or a documented spectral approximation;
|
|
942
|
+
2. optical transmission, either spectral data or an explicit T-stop approximation;
|
|
943
|
+
3. pupil/vignetting behavior;
|
|
944
|
+
4. photosite collection-area semantics;
|
|
945
|
+
5. exposure integration;
|
|
946
|
+
6. sensor spectral response / quantum efficiency.
|
|
947
|
+
|
|
948
|
+
Example:
|
|
949
|
+
|
|
950
|
+
```ts
|
|
951
|
+
import {
|
|
952
|
+
assessRadiometryReadiness,
|
|
953
|
+
parseRadiometryReadinessProfile
|
|
954
|
+
} from "@photivra/engine";
|
|
955
|
+
|
|
956
|
+
const profile = parseRadiometryReadinessProfile({
|
|
957
|
+
schemaVersion: "0.1.0",
|
|
958
|
+
components: [
|
|
959
|
+
{
|
|
960
|
+
requirement: "photosite-collection-area",
|
|
961
|
+
scientificStatus: "approximation",
|
|
962
|
+
modelId: "example-collection-area",
|
|
963
|
+
modelVersion: "1.0.0",
|
|
964
|
+
evidence: [
|
|
965
|
+
{
|
|
966
|
+
sourceOrigin: "photivra",
|
|
967
|
+
sourceReference: "photivra:example",
|
|
968
|
+
reuseStatus: "photivra-owned"
|
|
969
|
+
}
|
|
970
|
+
],
|
|
971
|
+
uncertainty: {
|
|
972
|
+
kind: "not-quantified",
|
|
973
|
+
limitation: "Example only."
|
|
974
|
+
},
|
|
975
|
+
areaModel: "geometric-area-times-fill-factor",
|
|
976
|
+
geometricCellAreaSquareMicrometers: 36,
|
|
977
|
+
fillFactor: 0.8
|
|
978
|
+
}
|
|
979
|
+
]
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
const readiness = assessRadiometryReadiness(profile);
|
|
983
|
+
|
|
984
|
+
console.log(readiness.status); // "not-ready"
|
|
985
|
+
console.log(readiness.missingRequirements);
|
|
986
|
+
console.log(readiness.composedPhotonOutputEnabled); // always false
|
|
987
|
+
```
|
|
988
|
+
|
|
989
|
+
Readiness states are:
|
|
990
|
+
|
|
991
|
+
- `not-ready`: one or more required prerequisite categories are missing;
|
|
992
|
+
- `approximate-only`: every category is present, but at least one component is approximate or has unquantified uncertainty;
|
|
993
|
+
- `calibrated-ready`: every category is declared calibrated and carries quantified uncertainty.
|
|
994
|
+
|
|
995
|
+
A readiness assessment validates the declared structure and provenance metadata. It does not verify that a source or calibration is scientifically correct.
|
|
996
|
+
|
|
997
|
+
Geometric sample pitch is **not** accepted as photon-collection area by itself. The profile must declare either an effective collection area or a geometric cell area plus explicit fill factor.
|
|
998
|
+
|
|
999
|
+
Data-bearing spectral/spatial calibration inputs are represented by an artifact ID plus SHA-256 checksum. Photivra does not infer reuse rights from public availability; reusable calibration data requires explicit licensing or Photivra ownership.
|
|
1000
|
+
|
|
1001
|
+
The readiness API does not calculate photons, does not change the existing `calculatePhotoelectrons()` primitive, and does not add photon/noise output to `simulatePocCamera()`. Enabling any composed photon model remains a separate future scientific/integration step.
|
|
1002
|
+
|
|
364
1003
|
## Photoelectron and SNR primitives
|
|
365
1004
|
|
|
366
1005
|
Use `calculatePhotoelectrons()` only when you already have a defensible mean incident photon count and quantum efficiency:
|
|
@@ -493,6 +1132,19 @@ See [Scientific and Source Provenance](PROVENANCE.md) and [Public API Style](API
|
|
|
493
1132
|
|
|
494
1133
|
Use `simulatePocCamera()` when you want one composed response containing the current POC calculations.
|
|
495
1134
|
|
|
1135
|
+
The root engine and composed POC are versioned independently. `ENGINE_API_VERSION` describes the root library surface; `POC_SIMULATION_API_VERSION` describes this composed request/response contract.
|
|
1136
|
+
|
|
1137
|
+
The current composed POC reports X/Y geometric sample pitch but still uses one backwards-compatible representative horizontal pitch internally for blur/sampling calculations. It therefore rejects sensor geometry whose X/Y pitch differs by more than 1%. Axis-aware lower-level geometry remains available for more general sensor layouts.
|
|
1138
|
+
|
|
1139
|
+
POC API 0.20 composes the capture-geometry foundation through final output/viewing semantics. Existing requests remain valid. New callers may supply an optional `capture` object with physical orientation, an optional native active-capture rectangle, an optional oriented output crop, and an optional final output raster.
|
|
1140
|
+
|
|
1141
|
+
Capture mode deliberately keeps legacy `crop.factor` separate: it must remain `1` when `capture` is present. Staged capture can use the existing `subjectCrop` request, but the result appears under `capture.subjectFraming` because it is a post-output framing stage rather than a legacy total-crop factor.
|
|
1142
|
+
|
|
1143
|
+
Equivalent-viewing CoC in capture mode uses the final retained physical image region, including subject framing when present; changing only output pixel resolution does not change the criterion. Explicit `circleOfConfusionMm` remains unchanged.
|
|
1144
|
+
|
|
1145
|
+
The optional response `capture` block exposes resolved geometry, active/output FOV, active-capture diagonal-based 35 mm-equivalent focal length, output sampling scale, optional subject framing, and explicit native-raster/oriented/output motion diagnostics. Legacy motion Y is image-plane +Y-up; capture raster Y is +Y-down, so the conversion is reported rather than hidden. Physical focal length remains authoritative. Sensor-architecture metadata and radiometry-readiness profiles are still standalone.
|
|
1146
|
+
|
|
1147
|
+
|
|
496
1148
|
```ts
|
|
497
1149
|
import { simulatePocCamera } from "@photivra/engine";
|
|
498
1150
|
|
|
@@ -553,7 +1205,66 @@ console.log(simulation.motion);
|
|
|
553
1205
|
console.log(simulation.provenance);
|
|
554
1206
|
```
|
|
555
1207
|
|
|
556
|
-
|
|
1208
|
+
To opt into staged capture geometry:
|
|
1209
|
+
|
|
1210
|
+
```ts
|
|
1211
|
+
const portraitCrop = simulatePocCamera({
|
|
1212
|
+
sensor: {
|
|
1213
|
+
widthMm: 36,
|
|
1214
|
+
heightMm: 24,
|
|
1215
|
+
pixelWidth: 6000,
|
|
1216
|
+
pixelHeight: 4000
|
|
1217
|
+
},
|
|
1218
|
+
lens: {
|
|
1219
|
+
focalLengthMm: 50,
|
|
1220
|
+
aperture: 4
|
|
1221
|
+
},
|
|
1222
|
+
exposure: {
|
|
1223
|
+
shutterSeconds: 1 / 250,
|
|
1224
|
+
iso: 100
|
|
1225
|
+
},
|
|
1226
|
+
focus: {
|
|
1227
|
+
focusDistanceM: 10,
|
|
1228
|
+
circleOfConfusionMm: 0.03
|
|
1229
|
+
},
|
|
1230
|
+
crop: {
|
|
1231
|
+
factor: 1
|
|
1232
|
+
},
|
|
1233
|
+
capture: {
|
|
1234
|
+
orientation: "portrait-clockwise",
|
|
1235
|
+
activeCaptureRect: {
|
|
1236
|
+
x: 1500,
|
|
1237
|
+
y: 1000,
|
|
1238
|
+
width: 3000,
|
|
1239
|
+
height: 2000
|
|
1240
|
+
},
|
|
1241
|
+
outputCropRect: {
|
|
1242
|
+
x: 500,
|
|
1243
|
+
y: 750,
|
|
1244
|
+
width: 1000,
|
|
1245
|
+
height: 1500
|
|
1246
|
+
},
|
|
1247
|
+
outputRaster: {
|
|
1248
|
+
pixelWidth: 2000,
|
|
1249
|
+
pixelHeight: 3000
|
|
1250
|
+
}
|
|
1251
|
+
},
|
|
1252
|
+
diffraction: {
|
|
1253
|
+
wavelengthNm: 550
|
|
1254
|
+
},
|
|
1255
|
+
motion: {
|
|
1256
|
+
positionM: { x: 0, y: 0, z: 10 },
|
|
1257
|
+
velocityMps: { x: 1, y: 0, z: 0 }
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
console.log(portraitCrop.capture?.geometry);
|
|
1262
|
+
console.log(portraitCrop.capture?.activeFieldOfView);
|
|
1263
|
+
console.log(portraitCrop.capture?.focalLength);
|
|
1264
|
+
console.log(portraitCrop.capture?.motion.outputDeltaPixels);
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
The focus request must supply exactly one circle-of-confusion criterion: either `circleOfConfusionMm` or `equivalentViewingCircleOfConfusion`. In staged capture mode, the equivalent-viewing approximation uses the final retained physical image region; an explicit `circleOfConfusionMm` remains unchanged by crop/output geometry.
|
|
557
1268
|
|
|
558
1269
|
Additional named defocus, sampling, and motion samples can be supplied when a renderer or analysis client needs per-object outputs.
|
|
559
1270
|
|