react-native-image-stitcher 0.2.1 → 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 +511 -1
- package/README.md +1 -1
- package/android/src/main/cpp/keyframe_gate_jni.cpp +138 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +412 -40
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +128 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +87 -45
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +46 -4
- package/cpp/stitcher.cpp +101 -1
- package/cpp/stitcher.hpp +8 -0
- package/dist/camera/Camera.d.ts +9 -0
- package/dist/camera/Camera.js +165 -43
- package/dist/camera/CaptureDebugOverlay.d.ts +45 -0
- package/dist/camera/CaptureDebugOverlay.js +146 -0
- package/dist/camera/CaptureKeyframePill.d.ts +28 -0
- package/dist/camera/CaptureKeyframePill.js +60 -0
- package/dist/camera/CaptureMemoryPill.d.ts +28 -0
- package/dist/camera/CaptureMemoryPill.js +109 -0
- package/dist/camera/CaptureOrientationPill.d.ts +22 -0
- package/dist/camera/CaptureOrientationPill.js +44 -0
- package/dist/camera/CaptureStitchStatsToast.d.ts +45 -0
- package/dist/camera/CaptureStitchStatsToast.js +133 -0
- package/dist/camera/PanoramaSettings.d.ts +478 -0
- package/dist/camera/PanoramaSettings.js +120 -0
- package/dist/camera/PanoramaSettingsBridge.d.ts +84 -0
- package/dist/camera/PanoramaSettingsBridge.js +208 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +50 -298
- package/dist/camera/PanoramaSettingsModal.js +189 -354
- package/dist/camera/buildPanoramaInitialSettings.d.ts +70 -0
- package/dist/camera/buildPanoramaInitialSettings.js +97 -0
- package/dist/camera/lowMemDevice.d.ts +24 -0
- package/dist/camera/lowMemDevice.js +69 -0
- package/dist/index.d.ts +16 -2
- package/dist/index.js +37 -2
- package/dist/sensors/useIMUTranslationGate.d.ts +26 -0
- package/dist/sensors/useIMUTranslationGate.js +83 -1
- package/dist/stitching/incremental.d.ts +25 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +12 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +321 -7
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +8 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +12 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +13 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +15 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +1 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +17 -4
- package/ios/Sources/RNImageStitcher/Stitcher.swift +6 -1
- package/package.json +6 -2
- package/src/camera/Camera.tsx +220 -54
- package/src/camera/CaptureDebugOverlay.tsx +180 -0
- package/src/camera/CaptureKeyframePill.tsx +77 -0
- package/src/camera/CaptureMemoryPill.tsx +96 -0
- package/src/camera/CaptureOrientationPill.tsx +57 -0
- package/src/camera/CaptureStitchStatsToast.tsx +155 -0
- package/src/camera/PanoramaSettings.ts +605 -0
- package/src/camera/PanoramaSettingsBridge.ts +238 -0
- package/src/camera/PanoramaSettingsModal.tsx +296 -988
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +375 -0
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +119 -0
- package/src/camera/__tests__/lowMemDevice.test.ts +52 -0
- package/src/camera/buildPanoramaInitialSettings.ts +139 -0
- package/src/camera/lowMemDevice.ts +71 -0
- package/src/index.ts +61 -3
- package/src/sensors/useIMUTranslationGate.ts +112 -1
- package/src/stitching/incremental.ts +25 -0
- package/src/stitching/useIncrementalStitcher.ts +18 -0
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the JS→native settings bridge.
|
|
4
|
+
*
|
|
5
|
+
* Scope: every adapter (panoramaSettingsToNativeConfig,
|
|
6
|
+
* slitscanSettingsToNativeConfig, hybridSettingsToNativeConfig)
|
|
7
|
+
* round-trip from a hierarchical typed input to the flat wire dict
|
|
8
|
+
* the native side reads. Asserts both:
|
|
9
|
+
*
|
|
10
|
+
* 1. Naming — JS key `registration.ncc1d.searchRadius` becomes
|
|
11
|
+
* native key `nccSearchRadius1d` (and similar mappings). Each
|
|
12
|
+
* DEFAULT_* snapshot's expected wire dict is enumerated below;
|
|
13
|
+
* drift in either direction (lib drops a key, or adds a phantom
|
|
14
|
+
* one) is caught here.
|
|
15
|
+
*
|
|
16
|
+
* 2. Presence-as-enable — undefined optional sub-objects in the
|
|
17
|
+
* typed shape (`registration.ncc1d`, `registration.ncc2d`,
|
|
18
|
+
* `registration.ncc2d.emaSmoothing`, `registration.ncc2d.panAxisLock`,
|
|
19
|
+
* `frameSelection.flow`, `advanced`) translate to explicit
|
|
20
|
+
* `enable*: false` (or the absence of all the sub-object's
|
|
21
|
+
* payload keys) on the wire. Many of these have been silent
|
|
22
|
+
* drift hazards historically — the old flat type required the
|
|
23
|
+
* consumer to set BOTH `enable1dNcc: true` AND `nccSearchRadius1d:
|
|
24
|
+
* <value>`; v0.4 makes them inseparable by collapsing into a
|
|
25
|
+
* single optional sub-object, and this file is what guarantees
|
|
26
|
+
* the wire side still gets both halves.
|
|
27
|
+
*
|
|
28
|
+
* 3. Engine-discriminated coverage — plane source variants
|
|
29
|
+
* ('Disabled' / 'ARKitDetected' / 'Virtual') gate which optional
|
|
30
|
+
* plane fields are emitted; the bridge filters those at the
|
|
31
|
+
* adapter boundary so the modal's per-source rendering doesn't
|
|
32
|
+
* get mislead by stale-but-present keys from a previous source
|
|
33
|
+
* selection.
|
|
34
|
+
*
|
|
35
|
+
* These tests are pure-TS; no React Native module import. Jest config
|
|
36
|
+
* (`jest.config.js`) routes test files in `__tests__/` through ts-jest
|
|
37
|
+
* with the `node` testEnvironment.
|
|
38
|
+
*/
|
|
39
|
+
|
|
40
|
+
import {
|
|
41
|
+
DEFAULT_FLOW_GATE_SETTINGS,
|
|
42
|
+
DEFAULT_HYBRID_SETTINGS,
|
|
43
|
+
DEFAULT_PANORAMA_SETTINGS,
|
|
44
|
+
DEFAULT_SLITSCAN_SETTINGS,
|
|
45
|
+
type HybridSettings,
|
|
46
|
+
type PanoramaSettings,
|
|
47
|
+
type SlitscanSettings,
|
|
48
|
+
} from '../PanoramaSettings';
|
|
49
|
+
import {
|
|
50
|
+
hybridSettingsToNativeConfig,
|
|
51
|
+
panoramaSettingsToNativeConfig,
|
|
52
|
+
slitscanSettingsToNativeConfig,
|
|
53
|
+
} from '../PanoramaSettingsBridge';
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
// ════════════════════════════════════════════════════════════════════
|
|
57
|
+
// PANORAMA — batch-keyframe engine
|
|
58
|
+
// ════════════════════════════════════════════════════════════════════
|
|
59
|
+
|
|
60
|
+
describe('panoramaSettingsToNativeConfig', () => {
|
|
61
|
+
it('round-trips DEFAULT_PANORAMA_SETTINGS to the expected flat dict', () => {
|
|
62
|
+
const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
|
|
63
|
+
|
|
64
|
+
// Cross-cutting
|
|
65
|
+
expect(cfg.captureSource).toBe('ar');
|
|
66
|
+
|
|
67
|
+
// BatchStitcherSettings
|
|
68
|
+
expect(cfg.stitchMode).toBe('auto');
|
|
69
|
+
expect(cfg.warperType).toBe('plane');
|
|
70
|
+
expect(cfg.blenderType).toBe('multiband');
|
|
71
|
+
expect(cfg.seamFinderType).toBe('graphcut');
|
|
72
|
+
expect(cfg.enableMaxInscribedRectCrop).toBe(false);
|
|
73
|
+
|
|
74
|
+
// FrameSelectionSettings
|
|
75
|
+
expect(cfg.frameSelectionMode).toBe('flow-based');
|
|
76
|
+
expect(cfg.keyframeMaxCount).toBe(6);
|
|
77
|
+
expect(cfg.keyframeOverlapThreshold).toBe(0.2);
|
|
78
|
+
|
|
79
|
+
// FlowGateSettings (flow is defined in the default)
|
|
80
|
+
expect(cfg.flowNoveltyPercentile).toBe(0.85);
|
|
81
|
+
expect(cfg.flowEvalEveryNFrames).toBe(5);
|
|
82
|
+
expect(cfg.flowMaxTranslationCm).toBe(50);
|
|
83
|
+
expect(cfg.flowMaxCorners).toBe(150);
|
|
84
|
+
expect(cfg.flowQualityLevel).toBe(0.01);
|
|
85
|
+
expect(cfg.flowMinDistance).toBe(10);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('falls back to DEFAULT_FLOW_GATE_SETTINGS when frameSelection.flow is undefined', () => {
|
|
89
|
+
// F10 Phase 2 review B1 — native compiled-in defaults disagree
|
|
90
|
+
// with the JS defaults for two flow knobs (maxTranslationCm and
|
|
91
|
+
// evalEveryNFrames). The bridge must always emit every flow key
|
|
92
|
+
// so sparse-literal hosts get the JS defaults on the wire, not
|
|
93
|
+
// the native fallbacks.
|
|
94
|
+
const noFlow: PanoramaSettings = {
|
|
95
|
+
...DEFAULT_PANORAMA_SETTINGS,
|
|
96
|
+
frameSelection: {
|
|
97
|
+
...DEFAULT_PANORAMA_SETTINGS.frameSelection,
|
|
98
|
+
flow: undefined,
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const cfg = panoramaSettingsToNativeConfig(noFlow);
|
|
102
|
+
|
|
103
|
+
expect(cfg.frameSelectionMode).toBe('flow-based');
|
|
104
|
+
expect(cfg.keyframeMaxCount).toBe(6);
|
|
105
|
+
expect(cfg.keyframeOverlapThreshold).toBe(0.2);
|
|
106
|
+
|
|
107
|
+
// Every flow.* native key present, matching DEFAULT_FLOW_GATE_SETTINGS.
|
|
108
|
+
expect(cfg.flowNoveltyPercentile).toBe(DEFAULT_FLOW_GATE_SETTINGS.noveltyPercentile);
|
|
109
|
+
expect(cfg.flowEvalEveryNFrames).toBe(DEFAULT_FLOW_GATE_SETTINGS.evalEveryNFrames);
|
|
110
|
+
expect(cfg.flowMaxTranslationCm).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxTranslationCm);
|
|
111
|
+
expect(cfg.flowMaxCorners).toBe(DEFAULT_FLOW_GATE_SETTINGS.maxCorners);
|
|
112
|
+
expect(cfg.flowQualityLevel).toBe(DEFAULT_FLOW_GATE_SETTINGS.qualityLevel);
|
|
113
|
+
expect(cfg.flowMinDistance).toBe(DEFAULT_FLOW_GATE_SETTINGS.minDistance);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('emits flow defaults to the wire when frameSelection.flow is undefined AND mode is flow-based', () => {
|
|
117
|
+
// F10 Phase 2 review N3 — the realistic user-facing case:
|
|
118
|
+
// host writes `mode: 'flow-based'` but omits the flow sub-tree.
|
|
119
|
+
// Pre-B1-fix, the gate would silently run with native fallbacks
|
|
120
|
+
// (flowMaxTranslationCm=0, flowEvalEveryNFrames=1) instead of
|
|
121
|
+
// the JS defaults (50 cm budget, 5× throttle).
|
|
122
|
+
const s: PanoramaSettings = {
|
|
123
|
+
...DEFAULT_PANORAMA_SETTINGS,
|
|
124
|
+
frameSelection: {
|
|
125
|
+
mode: 'flow-based',
|
|
126
|
+
maxKeyframes: 6,
|
|
127
|
+
overlapThreshold: 0.20,
|
|
128
|
+
// flow omitted — legal per the optional `?` in the type
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const cfg = panoramaSettingsToNativeConfig(s);
|
|
132
|
+
|
|
133
|
+
expect(cfg.flowMaxTranslationCm).toBe(50);
|
|
134
|
+
expect(cfg.flowEvalEveryNFrames).toBe(5);
|
|
135
|
+
expect(cfg.flowNoveltyPercentile).toBe(0.85);
|
|
136
|
+
expect(cfg.flowMaxCorners).toBe(150);
|
|
137
|
+
expect(cfg.flowQualityLevel).toBe(0.01);
|
|
138
|
+
expect(cfg.flowMinDistance).toBe(10);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('locks down the full wire-key set for DEFAULT_PANORAMA_SETTINGS', () => {
|
|
142
|
+
// F10 Phase 2 review N4 — mirror the hybrid test below. Lock
|
|
143
|
+
// down which keys leave the bridge so a future field accidentally
|
|
144
|
+
// riding along (e.g. `debug` being treated as a wire knob) fails
|
|
145
|
+
// this test immediately.
|
|
146
|
+
const cfg = panoramaSettingsToNativeConfig(DEFAULT_PANORAMA_SETTINGS);
|
|
147
|
+
expect(Object.keys(cfg).sort()).toEqual([
|
|
148
|
+
'blenderType',
|
|
149
|
+
'captureSource',
|
|
150
|
+
'enableMaxInscribedRectCrop',
|
|
151
|
+
'flowEvalEveryNFrames',
|
|
152
|
+
'flowMaxCorners',
|
|
153
|
+
'flowMaxTranslationCm',
|
|
154
|
+
'flowMinDistance',
|
|
155
|
+
'flowNoveltyPercentile',
|
|
156
|
+
'flowQualityLevel',
|
|
157
|
+
'frameSelectionMode',
|
|
158
|
+
'keyframeMaxCount',
|
|
159
|
+
'keyframeOverlapThreshold',
|
|
160
|
+
'seamFinderType',
|
|
161
|
+
'stitchMode',
|
|
162
|
+
'warperType',
|
|
163
|
+
]);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('honours captureSource and stitcher overrides', () => {
|
|
167
|
+
const overridden: PanoramaSettings = {
|
|
168
|
+
...DEFAULT_PANORAMA_SETTINGS,
|
|
169
|
+
captureSource: 'non-ar',
|
|
170
|
+
debug: true,
|
|
171
|
+
stitcher: {
|
|
172
|
+
stitchMode: 'scans',
|
|
173
|
+
warperType: 'spherical',
|
|
174
|
+
blenderType: 'feather',
|
|
175
|
+
seamFinderType: 'skip',
|
|
176
|
+
enableMaxInscribedRectCrop: true,
|
|
177
|
+
},
|
|
178
|
+
};
|
|
179
|
+
const cfg = panoramaSettingsToNativeConfig(overridden);
|
|
180
|
+
|
|
181
|
+
expect(cfg.captureSource).toBe('non-ar');
|
|
182
|
+
expect(cfg.stitchMode).toBe('scans');
|
|
183
|
+
expect(cfg.warperType).toBe('spherical');
|
|
184
|
+
expect(cfg.blenderType).toBe('feather');
|
|
185
|
+
expect(cfg.seamFinderType).toBe('skip');
|
|
186
|
+
expect(cfg.enableMaxInscribedRectCrop).toBe(true);
|
|
187
|
+
// Note: `debug` is intentionally NOT on the wire — it's a
|
|
188
|
+
// JS-side UI gate, not a native config knob. The bridge MUST
|
|
189
|
+
// omit it; if a future change starts emitting it, the modal's
|
|
190
|
+
// operator-facing semantics will silently drift.
|
|
191
|
+
expect(cfg).not.toHaveProperty('debug');
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
// ════════════════════════════════════════════════════════════════════
|
|
197
|
+
// SLITSCAN — Layer 2 slit-scan engines
|
|
198
|
+
// ════════════════════════════════════════════════════════════════════
|
|
199
|
+
|
|
200
|
+
describe('slitscanSettingsToNativeConfig', () => {
|
|
201
|
+
it('round-trips DEFAULT_SLITSCAN_SETTINGS to the expected flat dict', () => {
|
|
202
|
+
const cfg = slitscanSettingsToNativeConfig(DEFAULT_SLITSCAN_SETTINGS);
|
|
203
|
+
|
|
204
|
+
expect(cfg.captureSource).toBe('ar');
|
|
205
|
+
expect(cfg.engineVariant).toBe('slitscan-rotate');
|
|
206
|
+
|
|
207
|
+
// Painting
|
|
208
|
+
expect(cfg.paintMode).toBe('FirstPaintedWins');
|
|
209
|
+
expect(cfg.sliverPosition).toBe('Bottom');
|
|
210
|
+
expect(cfg.firstFrameFullFrame).toBe(true);
|
|
211
|
+
|
|
212
|
+
// Registration (explicit booleans)
|
|
213
|
+
expect(cfg.enableTriangulation).toBe(false);
|
|
214
|
+
expect(cfg.enableTriAccumulator).toBe(false);
|
|
215
|
+
expect(cfg.enableRansacHomography).toBe(false);
|
|
216
|
+
|
|
217
|
+
// Plane
|
|
218
|
+
expect(cfg.planeSource).toBe('ARKitDetected');
|
|
219
|
+
expect(cfg.planeProjectionStyle).toBe('Rectified');
|
|
220
|
+
expect(cfg.arkitPlaneAlignmentThreshold).toBe(0.6);
|
|
221
|
+
|
|
222
|
+
// ncc1d / ncc2d both omitted in defaults
|
|
223
|
+
expect(cfg.enable1dNcc).toBe(false);
|
|
224
|
+
expect(cfg.enable2dNcc).toBe(false);
|
|
225
|
+
expect(cfg).not.toHaveProperty('nccSearchRadius1d');
|
|
226
|
+
expect(cfg).not.toHaveProperty('nccSearchMargin2d');
|
|
227
|
+
expect(cfg).not.toHaveProperty('nccConfidenceThreshold2d');
|
|
228
|
+
expect(cfg).not.toHaveProperty('ncc2dEmaAlpha');
|
|
229
|
+
expect(cfg).not.toHaveProperty('ncc2dCrossAxisLockPx');
|
|
230
|
+
|
|
231
|
+
// Plane: ARKitDetected — alignmentThreshold present, virtual depth absent
|
|
232
|
+
expect(cfg).not.toHaveProperty('virtualPlaneDepthMeters');
|
|
233
|
+
|
|
234
|
+
// Advanced: not set in defaults
|
|
235
|
+
expect(cfg).not.toHaveProperty('kPanAxisFractionRect');
|
|
236
|
+
expect(cfg).not.toHaveProperty('kMinAcceptDeltaPx');
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('expands `registration.ncc1d` presence-as-enable correctly', () => {
|
|
240
|
+
const withNcc1d: SlitscanSettings = {
|
|
241
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
242
|
+
registration: {
|
|
243
|
+
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
244
|
+
ncc1d: { searchRadius: 25 },
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
const cfg = slitscanSettingsToNativeConfig(withNcc1d);
|
|
248
|
+
expect(cfg.enable1dNcc).toBe(true);
|
|
249
|
+
expect(cfg.nccSearchRadius1d).toBe(25);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('expands `registration.ncc2d` presence-as-enable with nested optionals', () => {
|
|
253
|
+
const withNcc2dFull: SlitscanSettings = {
|
|
254
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
255
|
+
registration: {
|
|
256
|
+
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
257
|
+
ncc2d: {
|
|
258
|
+
searchMargin: 14,
|
|
259
|
+
confidenceThreshold: 0.95,
|
|
260
|
+
emaSmoothing: { alpha: 0.5 },
|
|
261
|
+
panAxisLock: { crossAxisLockPx: 4 },
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
};
|
|
265
|
+
const cfg = slitscanSettingsToNativeConfig(withNcc2dFull);
|
|
266
|
+
|
|
267
|
+
expect(cfg.enable2dNcc).toBe(true);
|
|
268
|
+
expect(cfg.nccSearchMargin2d).toBe(14);
|
|
269
|
+
expect(cfg.nccConfidenceThreshold2d).toBe(0.95);
|
|
270
|
+
expect(cfg.enableNcc2dEmaSmoothing).toBe(true);
|
|
271
|
+
expect(cfg.ncc2dEmaAlpha).toBe(0.5);
|
|
272
|
+
expect(cfg.enableNcc2dPanAxisLock).toBe(true);
|
|
273
|
+
expect(cfg.ncc2dCrossAxisLockPx).toBe(4);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
it('honours ncc2d nested-optional absence (ema + panAxisLock undefined)', () => {
|
|
277
|
+
const withNcc2dBare: SlitscanSettings = {
|
|
278
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
279
|
+
registration: {
|
|
280
|
+
...DEFAULT_SLITSCAN_SETTINGS.registration,
|
|
281
|
+
ncc2d: {
|
|
282
|
+
searchMargin: 12,
|
|
283
|
+
confidenceThreshold: 0.99,
|
|
284
|
+
// emaSmoothing + panAxisLock omitted → enable-flag false, no payload
|
|
285
|
+
},
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
const cfg = slitscanSettingsToNativeConfig(withNcc2dBare);
|
|
289
|
+
|
|
290
|
+
expect(cfg.enable2dNcc).toBe(true);
|
|
291
|
+
expect(cfg.enableNcc2dEmaSmoothing).toBe(false);
|
|
292
|
+
expect(cfg.enableNcc2dPanAxisLock).toBe(false);
|
|
293
|
+
// Critical: payload keys for the disabled sub-features must NOT
|
|
294
|
+
// ride along — Native engine would treat them as authoritative
|
|
295
|
+
// even with the enable flag off (defensive against a native bug).
|
|
296
|
+
expect(cfg).not.toHaveProperty('ncc2dEmaAlpha');
|
|
297
|
+
expect(cfg).not.toHaveProperty('ncc2dCrossAxisLockPx');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it.each([
|
|
301
|
+
['Disabled', { virtualPlaneDepthMeters: false, arkitPlaneAlignmentThreshold: false, planeProjectionStyle: false }],
|
|
302
|
+
['Virtual', { virtualPlaneDepthMeters: true, arkitPlaneAlignmentThreshold: false, planeProjectionStyle: true }],
|
|
303
|
+
['ARKitDetected', { virtualPlaneDepthMeters: false, arkitPlaneAlignmentThreshold: true, planeProjectionStyle: true }],
|
|
304
|
+
] as const)(
|
|
305
|
+
'emits plane optionals consistent with source=%s',
|
|
306
|
+
(source, expected) => {
|
|
307
|
+
const s: SlitscanSettings = {
|
|
308
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
309
|
+
plane: {
|
|
310
|
+
source,
|
|
311
|
+
projectionStyle: 'Rectified',
|
|
312
|
+
virtualDepthMeters: 2.0,
|
|
313
|
+
alignmentThreshold: 0.7,
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
const cfg = slitscanSettingsToNativeConfig(s);
|
|
317
|
+
expect(cfg.planeSource).toBe(source);
|
|
318
|
+
expect('virtualPlaneDepthMeters' in cfg).toBe(expected.virtualPlaneDepthMeters);
|
|
319
|
+
expect('arkitPlaneAlignmentThreshold' in cfg).toBe(expected.arkitPlaneAlignmentThreshold);
|
|
320
|
+
expect('planeProjectionStyle' in cfg).toBe(expected.planeProjectionStyle);
|
|
321
|
+
},
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
it('emits `advanced` knobs only when explicitly set', () => {
|
|
325
|
+
const withAdvanced: SlitscanSettings = {
|
|
326
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
327
|
+
advanced: { panAxisFractionRect: 0.6, minAcceptDeltaPx: 30 },
|
|
328
|
+
};
|
|
329
|
+
const cfg = slitscanSettingsToNativeConfig(withAdvanced);
|
|
330
|
+
expect(cfg.kPanAxisFractionRect).toBe(0.6);
|
|
331
|
+
expect(cfg.kMinAcceptDeltaPx).toBe(30);
|
|
332
|
+
|
|
333
|
+
const onlyOne: SlitscanSettings = {
|
|
334
|
+
...DEFAULT_SLITSCAN_SETTINGS,
|
|
335
|
+
advanced: { panAxisFractionRect: 0.6 },
|
|
336
|
+
// minAcceptDeltaPx omitted within the sub-object
|
|
337
|
+
};
|
|
338
|
+
const cfgOne = slitscanSettingsToNativeConfig(onlyOne);
|
|
339
|
+
expect(cfgOne.kPanAxisFractionRect).toBe(0.6);
|
|
340
|
+
expect(cfgOne).not.toHaveProperty('kMinAcceptDeltaPx');
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
// ════════════════════════════════════════════════════════════════════
|
|
346
|
+
// HYBRID — RetaiLens live engine
|
|
347
|
+
// ════════════════════════════════════════════════════════════════════
|
|
348
|
+
|
|
349
|
+
describe('hybridSettingsToNativeConfig', () => {
|
|
350
|
+
it('round-trips DEFAULT_HYBRID_SETTINGS to the expected flat dict', () => {
|
|
351
|
+
const cfg = hybridSettingsToNativeConfig(DEFAULT_HYBRID_SETTINGS);
|
|
352
|
+
expect(cfg.captureSource).toBe('ar');
|
|
353
|
+
expect(cfg.hybridProjection).toBe('Planar');
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('honours projection override', () => {
|
|
357
|
+
const cyl: HybridSettings = {
|
|
358
|
+
...DEFAULT_HYBRID_SETTINGS,
|
|
359
|
+
projection: 'Cylindrical',
|
|
360
|
+
};
|
|
361
|
+
expect(hybridSettingsToNativeConfig(cyl).hybridProjection).toBe('Cylindrical');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('emits only the documented hybrid surface (debug is JS-only)', () => {
|
|
365
|
+
// Hybrid presets internally clobber most fields; the bridge
|
|
366
|
+
// deliberately keeps the wire surface minimal. This test guards
|
|
367
|
+
// against future drift where someone adds a hybrid setting to the
|
|
368
|
+
// bridge without first validating that the engine actually reads it.
|
|
369
|
+
const cfg = hybridSettingsToNativeConfig({
|
|
370
|
+
...DEFAULT_HYBRID_SETTINGS,
|
|
371
|
+
debug: true, // JS-only, must NOT reach the wire
|
|
372
|
+
});
|
|
373
|
+
expect(Object.keys(cfg).sort()).toEqual(['captureSource', 'hybridProjection']);
|
|
374
|
+
});
|
|
375
|
+
});
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for buildPanoramaInitialSettings — the prop→settings-tree
|
|
4
|
+
* translation that runs once at <Camera>'s mount.
|
|
5
|
+
*
|
|
6
|
+
* Coverage:
|
|
7
|
+
*
|
|
8
|
+
* - Defaults (no prop overrides) reproduce DEFAULT_PANORAMA_SETTINGS
|
|
9
|
+
* when `isLowMemDevice=false`.
|
|
10
|
+
* - `isLowMemDevice=true` swaps blender + seamFinder defaults to the
|
|
11
|
+
* feather+skip fallback; other fields unchanged.
|
|
12
|
+
* - Every prop override routes to its hierarchical path: stitchMode →
|
|
13
|
+
* stitcher.stitchMode, defaultFlowMaxTranslationCm →
|
|
14
|
+
* frameSelection.flow.maxTranslationCm, etc.
|
|
15
|
+
* - Partial overrides leave non-overridden fields at the default.
|
|
16
|
+
*
|
|
17
|
+
* Plus a "wire-format integration" check: the produced settings tree,
|
|
18
|
+
* fed through `panoramaSettingsToNativeConfig`, lands at the expected
|
|
19
|
+
* flat dict. This is the seam where the prop translation, the
|
|
20
|
+
* hierarchical tree, and the bridge all meet — verifying it here means
|
|
21
|
+
* <Camera>'s `incremental.start({ config })` call is correctly wired
|
|
22
|
+
* end-to-end on the JS side (on-device run remains the integration
|
|
23
|
+
* check across the JS/native boundary itself).
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import {
|
|
27
|
+
DEFAULT_PANORAMA_SETTINGS,
|
|
28
|
+
type PanoramaSettings,
|
|
29
|
+
} from '../PanoramaSettings';
|
|
30
|
+
import { panoramaSettingsToNativeConfig } from '../PanoramaSettingsBridge';
|
|
31
|
+
import { buildPanoramaInitialSettings } from '../buildPanoramaInitialSettings';
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
describe('buildPanoramaInitialSettings', () => {
|
|
35
|
+
it('returns DEFAULT_PANORAMA_SETTINGS verbatim when no overrides and not low-mem', () => {
|
|
36
|
+
const s = buildPanoramaInitialSettings({}, false);
|
|
37
|
+
expect(s).toEqual(DEFAULT_PANORAMA_SETTINGS);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('swaps blender + seamFinder for the low-mem fallback', () => {
|
|
41
|
+
const s = buildPanoramaInitialSettings({}, true);
|
|
42
|
+
expect(s.stitcher.blenderType).toBe('feather');
|
|
43
|
+
expect(s.stitcher.seamFinderType).toBe('skip');
|
|
44
|
+
// Everything else stays at the static default.
|
|
45
|
+
expect(s.stitcher.stitchMode).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.stitchMode);
|
|
46
|
+
expect(s.stitcher.warperType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.warperType);
|
|
47
|
+
expect(s.frameSelection).toEqual(DEFAULT_PANORAMA_SETTINGS.frameSelection);
|
|
48
|
+
expect(s.captureSource).toBe(DEFAULT_PANORAMA_SETTINGS.captureSource);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('routes every prop override to its hierarchical path', () => {
|
|
52
|
+
const s = buildPanoramaInitialSettings(
|
|
53
|
+
{
|
|
54
|
+
defaultCaptureSource: 'non-ar',
|
|
55
|
+
defaultStitchMode: 'scans',
|
|
56
|
+
defaultBlender: 'feather',
|
|
57
|
+
defaultSeamFinder: 'skip',
|
|
58
|
+
defaultWarper: 'cylindrical',
|
|
59
|
+
defaultFlowNoveltyPercentile: 0.70,
|
|
60
|
+
defaultFlowEvalEveryNFrames: 3,
|
|
61
|
+
defaultFlowMaxTranslationCm: 12,
|
|
62
|
+
defaultKeyframeMaxCount: 8,
|
|
63
|
+
defaultKeyframeOverlapThreshold: 0.30,
|
|
64
|
+
},
|
|
65
|
+
false,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(s.captureSource).toBe('non-ar');
|
|
69
|
+
expect(s.stitcher.stitchMode).toBe('scans');
|
|
70
|
+
expect(s.stitcher.blenderType).toBe('feather');
|
|
71
|
+
expect(s.stitcher.seamFinderType).toBe('skip');
|
|
72
|
+
expect(s.stitcher.warperType).toBe('cylindrical');
|
|
73
|
+
expect(s.frameSelection.flow?.noveltyPercentile).toBe(0.70);
|
|
74
|
+
expect(s.frameSelection.flow?.evalEveryNFrames).toBe(3);
|
|
75
|
+
expect(s.frameSelection.flow?.maxTranslationCm).toBe(12);
|
|
76
|
+
expect(s.frameSelection.maxKeyframes).toBe(8);
|
|
77
|
+
expect(s.frameSelection.overlapThreshold).toBe(0.30);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('leaves non-overridden fields at the default (partial override)', () => {
|
|
81
|
+
const s = buildPanoramaInitialSettings(
|
|
82
|
+
{ defaultStitchMode: 'panorama' },
|
|
83
|
+
false,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
// The override took effect …
|
|
87
|
+
expect(s.stitcher.stitchMode).toBe('panorama');
|
|
88
|
+
|
|
89
|
+
// … and every other field stays at the corresponding default.
|
|
90
|
+
expect(s.stitcher.warperType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.warperType);
|
|
91
|
+
expect(s.stitcher.blenderType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.blenderType);
|
|
92
|
+
expect(s.stitcher.seamFinderType).toBe(DEFAULT_PANORAMA_SETTINGS.stitcher.seamFinderType);
|
|
93
|
+
expect(s.frameSelection).toEqual(DEFAULT_PANORAMA_SETTINGS.frameSelection);
|
|
94
|
+
expect(s.captureSource).toBe(DEFAULT_PANORAMA_SETTINGS.captureSource);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('produces wire-format-clean output when piped through the bridge', () => {
|
|
98
|
+
// The end-to-end JS-side path: props → buildPanoramaInitialSettings →
|
|
99
|
+
// panoramaSettingsToNativeConfig. Verifying it here catches drift
|
|
100
|
+
// at any of the three layers (prop name, type-tree shape, bridge
|
|
101
|
+
// adapter) with a single assertion.
|
|
102
|
+
const overrides = {
|
|
103
|
+
defaultCaptureSource: 'non-ar' as const,
|
|
104
|
+
defaultStitchMode: 'scans' as const,
|
|
105
|
+
defaultFlowMaxTranslationCm: 25,
|
|
106
|
+
};
|
|
107
|
+
const settings: PanoramaSettings =
|
|
108
|
+
buildPanoramaInitialSettings(overrides, false);
|
|
109
|
+
const wire = panoramaSettingsToNativeConfig(settings);
|
|
110
|
+
|
|
111
|
+
expect(wire.captureSource).toBe('non-ar');
|
|
112
|
+
expect(wire.stitchMode).toBe('scans');
|
|
113
|
+
expect(wire.flowMaxTranslationCm).toBe(25);
|
|
114
|
+
// Defaulted fields still on the wire with their default value.
|
|
115
|
+
expect(wire.warperType).toBe('plane');
|
|
116
|
+
expect(wire.frameSelectionMode).toBe('flow-based');
|
|
117
|
+
expect(wire.flowNoveltyPercentile).toBe(0.85);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* Unit tests for the pure `isBelowMemThreshold` classifier.
|
|
4
|
+
*
|
|
5
|
+
* The other two exports (`getPhysicalMemoryBytes` and `isLowMemDevice`)
|
|
6
|
+
* read the React Native bridge and can only be exercised on a real
|
|
7
|
+
* device. This file covers the threshold logic exhaustively so the
|
|
8
|
+
* classification rule is unit-tested without needing the RN runtime.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
LOW_MEM_THRESHOLD_BYTES,
|
|
13
|
+
isBelowMemThreshold,
|
|
14
|
+
} from '../lowMemDevice';
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
describe('isBelowMemThreshold', () => {
|
|
18
|
+
it('returns true for positive byte counts below the threshold', () => {
|
|
19
|
+
expect(isBelowMemThreshold(1)).toBe(true);
|
|
20
|
+
expect(isBelowMemThreshold(1024)).toBe(true);
|
|
21
|
+
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES - 1)).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('returns false at exactly the threshold (strict < comparison)', () => {
|
|
25
|
+
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES)).toBe(false);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('returns false for byte counts above the threshold', () => {
|
|
29
|
+
expect(isBelowMemThreshold(LOW_MEM_THRESHOLD_BYTES + 1)).toBe(false);
|
|
30
|
+
expect(isBelowMemThreshold(4 * 1024 * 1024 * 1024)).toBe(false);
|
|
31
|
+
expect(isBelowMemThreshold(Number.MAX_SAFE_INTEGER)).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('returns false for zero (unknown — safe default to high-quality combo)', () => {
|
|
35
|
+
expect(isBelowMemThreshold(0)).toBe(false);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('returns false for negative values (defensive)', () => {
|
|
39
|
+
expect(isBelowMemThreshold(-1)).toBe(false);
|
|
40
|
+
expect(isBelowMemThreshold(-LOW_MEM_THRESHOLD_BYTES)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('returns false for non-finite values (NaN / Infinity)', () => {
|
|
44
|
+
expect(isBelowMemThreshold(Number.NaN)).toBe(false);
|
|
45
|
+
expect(isBelowMemThreshold(Number.POSITIVE_INFINITY)).toBe(false);
|
|
46
|
+
expect(isBelowMemThreshold(Number.NEGATIVE_INFINITY)).toBe(false);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('threshold is exactly 2 GB', () => {
|
|
50
|
+
expect(LOW_MEM_THRESHOLD_BYTES).toBe(2 * 1024 * 1024 * 1024);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
/**
|
|
3
|
+
* buildPanoramaInitialSettings — pure helper that materialises the
|
|
4
|
+
* initial `PanoramaSettings` snapshot from <Camera>'s `default*` props
|
|
5
|
+
* and a device-capability hint.
|
|
6
|
+
*
|
|
7
|
+
* Why a separate file?
|
|
8
|
+
* ────────────────────
|
|
9
|
+
*
|
|
10
|
+
* The settings tree lives in `PanoramaSettings.ts`; <Camera> consumes
|
|
11
|
+
* it and writes it into React state. The translation FROM the prop
|
|
12
|
+
* surface (flat names like `defaultStitchMode`) INTO the hierarchical
|
|
13
|
+
* settings tree is the part that:
|
|
14
|
+
*
|
|
15
|
+
* • is non-trivial enough to deserve direct unit-test coverage
|
|
16
|
+
* (covers the prop→sub-tree path mapping, which is easy to drift),
|
|
17
|
+
* • is pure TS — no React, no React Native — so the test runs in
|
|
18
|
+
* jest's `node` environment without needing the `react-native`
|
|
19
|
+
* preset (the rest of <Camera> is unmockable in pure TS).
|
|
20
|
+
*
|
|
21
|
+
* Living alongside `Camera.tsx` (vs. burying it as a private function
|
|
22
|
+
* inside) is the only way to get those two properties without taking
|
|
23
|
+
* on full React-Native jest setup just for this one helper.
|
|
24
|
+
*
|
|
25
|
+
* The exported `PanoramaPropOverrides` type is the prop-fragment
|
|
26
|
+
* <Camera> uses; `CameraProps` extends it. Keeping it explicit here
|
|
27
|
+
* means future Camera prop additions don't accidentally widen the
|
|
28
|
+
* settings-translation surface — every consumer of the helper sees
|
|
29
|
+
* exactly the prop fields that drive the settings tree.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import {
|
|
33
|
+
DEFAULT_FLOW_GATE_SETTINGS,
|
|
34
|
+
DEFAULT_PANORAMA_SETTINGS,
|
|
35
|
+
type PanoramaSettings,
|
|
36
|
+
} from './PanoramaSettings';
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Subset of <Camera>'s props that map onto fields of the initial
|
|
41
|
+
* `PanoramaSettings` snapshot. Anything outside this interface
|
|
42
|
+
* (e.g. `defaultLens`, `enablePhotoMode`, callbacks) is irrelevant
|
|
43
|
+
* to the settings shape and stays in `CameraProps` only.
|
|
44
|
+
*
|
|
45
|
+
* Forward-looking `default*ResolMP` props are documented here but
|
|
46
|
+
* intentionally not translated yet — the new `PanoramaSettings` tree
|
|
47
|
+
* has no home for them (the v0.3 audit found cv::Stitcher's resol
|
|
48
|
+
* knobs aren't reached by the current native bridges).
|
|
49
|
+
*/
|
|
50
|
+
export interface PanoramaPropOverrides {
|
|
51
|
+
defaultCaptureSource?: 'ar' | 'non-ar';
|
|
52
|
+
defaultStitchMode?: 'auto' | 'panorama' | 'scans';
|
|
53
|
+
defaultBlender?: 'multiband' | 'feather';
|
|
54
|
+
defaultSeamFinder?: 'graphcut' | 'skip';
|
|
55
|
+
defaultWarper?: 'plane' | 'cylindrical' | 'spherical';
|
|
56
|
+
defaultFlowNoveltyPercentile?: number;
|
|
57
|
+
defaultFlowEvalEveryNFrames?: number;
|
|
58
|
+
defaultFlowMaxTranslationCm?: number;
|
|
59
|
+
defaultKeyframeMaxCount?: number;
|
|
60
|
+
defaultKeyframeOverlapThreshold?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether this device is low-memory enough to benefit from the
|
|
66
|
+
* feather+skip blender/seam fallback (vs. the heavier multiband+
|
|
67
|
+
* graphcut default). <Camera> derives this from
|
|
68
|
+
* `NativeModules.BatchStitcher.physicalMemoryBytes` at module load
|
|
69
|
+
* (RN-only — see `getIsLowMemDevice` in Camera.tsx); tests pass
|
|
70
|
+
* `false` explicitly to keep the prop-translation path the unit of
|
|
71
|
+
* the unit test.
|
|
72
|
+
*
|
|
73
|
+
* Why a parameter and not a constant import?
|
|
74
|
+
* The pre-v0.4 `DEFAULT_PANORAMA_SETTINGS` was a `let` mutated at
|
|
75
|
+
* module load — side-effect-heavy, untestable. v0.4 keeps the
|
|
76
|
+
* defaults static + side-effect-free; the device adaptation lives
|
|
77
|
+
* exactly where it needs to (Camera's mount-time `useState`).
|
|
78
|
+
*/
|
|
79
|
+
export function buildPanoramaInitialSettings(
|
|
80
|
+
overrides: PanoramaPropOverrides,
|
|
81
|
+
isLowMemDevice: boolean,
|
|
82
|
+
): PanoramaSettings {
|
|
83
|
+
// Start from the static, side-effect-free defaults.
|
|
84
|
+
const base = DEFAULT_PANORAMA_SETTINGS;
|
|
85
|
+
|
|
86
|
+
// Apply the low-memory device adaptation:
|
|
87
|
+
// - feather blender (streams warped frames, no peak-memory spike)
|
|
88
|
+
// - skip seam finder (no graphcut working set)
|
|
89
|
+
// Replaces the v0.3 module-load-time mutation; same semantics.
|
|
90
|
+
const stitcherDefaults = isLowMemDevice
|
|
91
|
+
? {
|
|
92
|
+
...base.stitcher,
|
|
93
|
+
blenderType: 'feather' as const,
|
|
94
|
+
seamFinderType: 'skip' as const,
|
|
95
|
+
}
|
|
96
|
+
: base.stitcher;
|
|
97
|
+
|
|
98
|
+
// Use the standalone DEFAULT_FLOW_GATE_SETTINGS constant rather
|
|
99
|
+
// than `base.frameSelection.flow!` — the non-null assertion would
|
|
100
|
+
// crash silently if a future refactor un-defines the default's
|
|
101
|
+
// flow sub-tree, but the constant lives at the same level as the
|
|
102
|
+
// type and is type-checked. See F10 Phase 2 review (NIT-4).
|
|
103
|
+
const flowDefaults = DEFAULT_FLOW_GATE_SETTINGS;
|
|
104
|
+
|
|
105
|
+
return {
|
|
106
|
+
captureSource: overrides.defaultCaptureSource ?? base.captureSource,
|
|
107
|
+
debug: base.debug,
|
|
108
|
+
|
|
109
|
+
stitcher: {
|
|
110
|
+
...stitcherDefaults,
|
|
111
|
+
stitchMode: overrides.defaultStitchMode ?? stitcherDefaults.stitchMode,
|
|
112
|
+
warperType: overrides.defaultWarper ?? stitcherDefaults.warperType,
|
|
113
|
+
blenderType: overrides.defaultBlender ?? stitcherDefaults.blenderType,
|
|
114
|
+
seamFinderType:
|
|
115
|
+
overrides.defaultSeamFinder ?? stitcherDefaults.seamFinderType,
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
frameSelection: {
|
|
119
|
+
...base.frameSelection,
|
|
120
|
+
maxKeyframes:
|
|
121
|
+
overrides.defaultKeyframeMaxCount ?? base.frameSelection.maxKeyframes,
|
|
122
|
+
overlapThreshold:
|
|
123
|
+
overrides.defaultKeyframeOverlapThreshold
|
|
124
|
+
?? base.frameSelection.overlapThreshold,
|
|
125
|
+
flow: {
|
|
126
|
+
...flowDefaults,
|
|
127
|
+
noveltyPercentile:
|
|
128
|
+
overrides.defaultFlowNoveltyPercentile
|
|
129
|
+
?? flowDefaults.noveltyPercentile,
|
|
130
|
+
evalEveryNFrames:
|
|
131
|
+
overrides.defaultFlowEvalEveryNFrames
|
|
132
|
+
?? flowDefaults.evalEveryNFrames,
|
|
133
|
+
maxTranslationCm:
|
|
134
|
+
overrides.defaultFlowMaxTranslationCm
|
|
135
|
+
?? flowDefaults.maxTranslationCm,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|