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
|
@@ -1,21 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
// SPDX-License-Identifier: Apache-2.0
|
|
3
3
|
/**
|
|
4
|
-
* PanoramaSettingsModal — runtime
|
|
5
|
-
*
|
|
6
|
-
* blender, and tuning constants between captures to see what
|
|
7
|
-
* looks best on real shelf scenes.
|
|
4
|
+
* PanoramaSettingsModal — runtime tuning surface for <Camera>'s
|
|
5
|
+
* batch-keyframe panorama capture.
|
|
8
6
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* with `visible` toggled by a gear-icon press in the capture
|
|
12
|
-
* header. Settings flow OUT via `onChange` for each tweak.
|
|
7
|
+
* v0.4 rewrite (Phase 2 of F10):
|
|
8
|
+
* ──────────────────────────────
|
|
13
9
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* The v0.3 modal exposed a flat 45-field surface that mixed
|
|
11
|
+
* batch-keyframe knobs with slit-scan, hybrid, and video-recording
|
|
12
|
+
* fallback fields the engine never reads in <Camera>'s
|
|
13
|
+
* `engine: 'batch-keyframe'` path. The 2026-05-22 audit (v0.3.0
|
|
14
|
+
* CHANGELOG) traced every field's native consumer and proved most of
|
|
15
|
+
* the cross-engine fields were dead surface in this modal.
|
|
16
|
+
*
|
|
17
|
+
* v0.4 narrows the modal to exactly the surface <Camera> consumes:
|
|
18
|
+
* the `PanoramaSettings` type defined in `./PanoramaSettings.ts`. Each
|
|
19
|
+
* section in the modal mirrors a sub-tree of that type — operators see
|
|
20
|
+
* the same shape in the UI as the code, and host apps that want to
|
|
21
|
+
* tune slit-scan or hybrid engines build their own analogous
|
|
22
|
+
* SlitscanSettingsModal / HybridSettingsModal on top of those types.
|
|
23
|
+
*
|
|
24
|
+
* UI structure (matches the type tree):
|
|
25
|
+
*
|
|
26
|
+
* - Debug (top-level, `debug`)
|
|
27
|
+
* - Frame selection (`frameSelection`, closed by default)
|
|
28
|
+
* - Mode
|
|
29
|
+
* - Max keyframes
|
|
30
|
+
* - Overlap threshold
|
|
31
|
+
* - Flow tunables (`frameSelection.flow`, only when
|
|
32
|
+
* mode === 'flow-based')
|
|
33
|
+
* - Max corners
|
|
34
|
+
* - Quality level
|
|
35
|
+
* - Min distance
|
|
36
|
+
* - Max translation cm
|
|
37
|
+
* - Novelty percentile
|
|
38
|
+
* - Eval every N frames
|
|
39
|
+
* - Stitcher (`stitcher`, closed by default)
|
|
40
|
+
* - Stitch mode
|
|
41
|
+
* - Warper type
|
|
42
|
+
* - Blender
|
|
43
|
+
* - Seam finder
|
|
44
|
+
* - Inscribed-rect crop
|
|
45
|
+
* - Reset to defaults (button)
|
|
46
|
+
*
|
|
47
|
+
* Note: `captureSource` (AR vs non-AR) is NOT surfaced here. The
|
|
48
|
+
* camera-screen AR toggle owns that state — Camera.tsx overrides the
|
|
49
|
+
* native bridge's `captureSource` with the derived
|
|
50
|
+
* `effectiveCaptureSource` so settings and runtime stay in sync.
|
|
51
|
+
*
|
|
52
|
+
* The reusable `Accordion` + `SectionHeader` + `SegmentedControl` +
|
|
53
|
+
* `Tag` helpers from the v0.3 modal are preserved verbatim — only the
|
|
54
|
+
* data-binding layer changed.
|
|
19
55
|
*/
|
|
20
56
|
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
21
57
|
if (k2 === undefined) k2 = k;
|
|
@@ -51,211 +87,64 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
51
87
|
};
|
|
52
88
|
})();
|
|
53
89
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
54
|
-
exports.DEFAULT_PANORAMA_SETTINGS = void 0;
|
|
55
90
|
exports.PanoramaSettingsModal = PanoramaSettingsModal;
|
|
56
91
|
const react_1 = __importStar(require("react"));
|
|
57
92
|
const react_native_1 = require("react-native");
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
: undefined;
|
|
71
|
-
return typeof bytes === 'number' ? bytes : 0;
|
|
72
|
-
})();
|
|
73
|
-
const _isLowMem = _physicalMemoryBytes > 0
|
|
74
|
-
&& _physicalMemoryBytes < 2 * 1024 * 1024 * 1024;
|
|
75
|
-
// One-line diagnostic so the host's Metro console shows what the
|
|
76
|
-
// SDK saw at module load. If `physicalMemoryBytes=0` here, the
|
|
77
|
-
// native bridge's `constantsToExport` isn't being picked up by
|
|
78
|
-
// React Native and we should investigate the @objc registration.
|
|
79
|
-
// The defaults always pick the SAFE fallback (multiband+graphcut)
|
|
80
|
-
// when the value is 0 — this log is the only signal we have.
|
|
81
|
-
// eslint-disable-next-line no-console
|
|
82
|
-
console.log('[capture-sdk] PanoramaSettings defaults: '
|
|
83
|
-
+ `physicalMemoryBytes=${_physicalMemoryBytes} `
|
|
84
|
-
+ `isLowMem=${_isLowMem} `
|
|
85
|
-
+ `→ blender=${_isLowMem ? 'feather' : 'multiband'} `
|
|
86
|
-
+ `seam=${_isLowMem ? 'skip' : 'graphcut'}`);
|
|
87
|
-
exports.DEFAULT_PANORAMA_SETTINGS = {
|
|
88
|
-
warperType: 'plane',
|
|
89
|
-
// High-quality defaults on devices with ≥2 GB RAM (iPhone X+):
|
|
90
|
-
// MultiBandBlender + GraphCutSeamFinder, the same combo
|
|
91
|
-
// cv::Stitcher::PANORAMA uses internally and what produced the
|
|
92
|
-
// sharpest output during iteration.
|
|
93
|
-
// Low-memory devices (<2 GB) fall back to FeatherBlender + skip
|
|
94
|
-
// seam (streams warp+feed) so peak memory stays under the
|
|
95
|
-
// tighter jetsam threshold. Either way, the user can switch
|
|
96
|
-
// both in the settings modal.
|
|
97
|
-
blenderType: _isLowMem ? 'feather' : 'multiband',
|
|
98
|
-
seamFinderType: _isLowMem ? 'skip' : 'graphcut',
|
|
99
|
-
// V16 Phase 1b.fix5c — default OFF. See PanoramaSettings.enableMaxInscribedRectCrop.
|
|
100
|
-
enableMaxInscribedRectCrop: false,
|
|
101
|
-
// AR-backed capture is the default — vision-camera path is kept as
|
|
102
|
-
// a fallback while we shake out edge cases.
|
|
103
|
-
useARPreview: true,
|
|
104
|
-
// V16 Phase 1 — batch-keyframe is the new default-recommended
|
|
105
|
-
// engine: KeyframeGate caps input at ≤ keyframeMaxCount frames,
|
|
106
|
-
// OpenCVStitcher's BA + GraphCut + ExposureCompensator +
|
|
107
|
-
// MultiBandBlender runs once on shutter release. Existing
|
|
108
|
-
// slitscan-* engines remain available for wide-pan fallback.
|
|
109
|
-
incrementalEngine: 'batch-keyframe',
|
|
110
|
-
slitWidthFraction: 0.30,
|
|
111
|
-
acceptGate: 0,
|
|
112
|
-
enableTriangulation: false,
|
|
113
|
-
enableTriAccumulator: false,
|
|
114
|
-
enable2dNcc: false,
|
|
115
|
-
enableRansacHomography: false,
|
|
116
|
-
// V15.0c — Ram observation: FirstPaintedWins is consistently the best
|
|
117
|
-
// output across all combinations. Default switched from FeatherBlend.
|
|
118
|
-
paintMode: 'FirstPaintedWins',
|
|
119
|
-
hybridProjection: 'Planar',
|
|
120
|
-
nccSearchRadius1d: 15,
|
|
121
|
-
useDetectedPlane: false,
|
|
122
|
-
// V16 Phase 1 — Virtual plane is the default since batch-keyframe
|
|
123
|
-
// is the recommended engine and the gate needs a plane to compute
|
|
124
|
-
// polygon overlap. Virtual works without ARKit-detected planes (a
|
|
125
|
-
// synthesized plane perpendicular to the first-frame camera at
|
|
126
|
-
// virtualPlaneDepthMeters); operators can flip to ARKitDetected
|
|
127
|
-
// when in a controlled scene with a clearly-visible wall. Disabled
|
|
128
|
-
// is still selectable for the older slit-scan paths that don't
|
|
129
|
-
// need a plane.
|
|
130
|
-
// V16 Phase 1b.fix5c (Ram's call 2026-05-10): switched default
|
|
131
|
-
// from 'Virtual' to 'ARKitDetected'. ARKit's real plane gives
|
|
132
|
-
// better intrinsics-to-pixel alignment than a synthesised plane
|
|
133
|
-
// at a fixed depth, when ARKit can find a vertical plane. Falls
|
|
134
|
-
// back to slit-scan when no plane latches.
|
|
135
|
-
planeSource: 'ARKitDetected',
|
|
136
|
-
virtualPlaneDepthMeters: 1.5,
|
|
137
|
-
arkitPlaneAlignmentThreshold: 0.6,
|
|
138
|
-
// V15.0g — Rectified is the default (Trapezoidal had the tilt-
|
|
139
|
-
// induced bottom-wider-than-top distortion that was the field
|
|
140
|
-
// blocker on V15.0e/f). Trapezoidal stays available for
|
|
141
|
-
// operator A/B comparison.
|
|
142
|
-
planeProjectionStyle: 'Rectified',
|
|
143
|
-
// V15.0d — NCC 2D defaults match V15.0c.4's hardcoded values, now
|
|
144
|
-
// tunable via the settings UI. EMA smoothing and pan-axis lock are
|
|
145
|
-
// off by default so the V15.0c.4 baseline behaviour is preserved
|
|
146
|
-
// until the operator explicitly opts in.
|
|
147
|
-
nccSearchMargin2d: 12,
|
|
148
|
-
// V15.0i.1 — default raised to 0.99 per Ram (only apply on near-
|
|
149
|
-
// perfect overlap matches; reject ambiguous matches that snap to
|
|
150
|
-
// wrong patterns on repetitive textures like shelf rails).
|
|
151
|
-
nccConfidenceThreshold2d: 0.99,
|
|
152
|
-
enableNcc2dEmaSmoothing: false,
|
|
153
|
-
ncc2dEmaAlpha: 0.4,
|
|
154
|
-
enableNcc2dPanAxisLock: false,
|
|
155
|
-
ncc2dCrossAxisLockPx: 5,
|
|
156
|
-
// V16 A2 (2026-05-13) — flow-based is now the default. Ram report
|
|
157
|
-
// 2026-05-13 13:05 showed that pose-based on a small latched plane
|
|
158
|
-
// produces "bursts" of accepts on small physical motion: a 0.64 m²
|
|
159
|
-
// plane at 2.7 m perpDist gave 6 accepts in 1 s over 12 cm of
|
|
160
|
-
// translation because the plane-projected polygon covers only a
|
|
161
|
-
// sliver of the frame, hyperinflating newContent. Flow-based
|
|
162
|
-
// measures novelty from real image content (sparse KLT), is
|
|
163
|
-
// plane-independent, and is invariant to plane size. Operators
|
|
164
|
-
// can still flip back to 'pose-based' or 'time-based' in the modal
|
|
165
|
-
// for A/B testing or low-texture scenes. Same defaults shared
|
|
166
|
-
// between pose-based and flow-based (40 % new content per
|
|
167
|
-
// keyframe, ≤ 6 keyframes per capture).
|
|
168
|
-
frameSelectionMode: 'flow-based',
|
|
169
|
-
// 2026-05-15 (U4) — flow-based default novelty 0.40 → 0.20.
|
|
170
|
-
// Accept frames with 20 % new content (was 40 %). More inclusive
|
|
171
|
-
// selection for shelf-pan captures where panning slowly produces
|
|
172
|
-
// gradual content reveal. Operator can still bump via Settings.
|
|
173
|
-
keyframeOverlapThreshold: 0.20,
|
|
174
|
-
keyframeMaxCount: 6,
|
|
175
|
-
// V16 A2 — flow-based mode tuning. Defaults are the values that
|
|
176
|
-
// tested cleanly on iPhone 13 Pro / 14 Pro: 150 corners give a
|
|
177
|
-
// stable median across the frame; quality=0.01 + minDistance=10
|
|
178
|
-
// give spatially-spread, repeatable detection. All three are
|
|
179
|
-
// tunable in the modal under "Flow tuning".
|
|
180
|
-
flowMaxCorners: 150,
|
|
181
|
-
flowQualityLevel: 0.01,
|
|
182
|
-
flowMinDistance: 10,
|
|
183
|
-
// V16 — translation-budget force-accept (Flow strategy only).
|
|
184
|
-
// 2026-05-16 (Issue 4a fix) — default flipped from 0 (disabled) to
|
|
185
|
-
// 25 cm so the "Rotate the camera instead of moving it sideways"
|
|
186
|
-
// warning fires out-of-the-box. Set to 0 in Settings to disable
|
|
187
|
-
// both the warning AND the gate's force-accept on budget crossing.
|
|
188
|
-
// 2026-05-17 (Issue 4-A v2) — raised 25 → 50 cm. The 25-cm budget
|
|
189
|
-
// was too tight given IMU double-integration drift (the
|
|
190
|
-
// accelerometer's noise floor accumulates several cm of bogus
|
|
191
|
-
// "translation" per second even when the phone is held still).
|
|
192
|
-
// Combined with the new `resetAnchor` at handleHoldStart (so drift
|
|
193
|
-
// doesn't compound across captures), 50 cm gives the warning real
|
|
194
|
-
// headroom for genuine sideways motion without false positives.
|
|
195
|
-
flowMaxTranslationCm: 50,
|
|
196
|
-
// V16 — novelty aggregation percentile. 0.85 picks up leading-
|
|
197
|
-
// edge motion sooner than the pre-V16 median (0.50). Operator
|
|
198
|
-
// can dial down toward 0.5 for more-conservative captures or up
|
|
199
|
-
// toward 0.99 for more-aggressive.
|
|
200
|
-
flowNoveltyPercentile: 0.85,
|
|
201
|
-
// V16 — every-Nth-frame eval throttle. 2026-05-15 (U4): default
|
|
202
|
-
// 1 → 5 to reduce per-frame KeyframeGate CPU cost (Shi-Tomasi +
|
|
203
|
-
// calcOpticalFlowPyrLK is ~3-5 ms per ARFrame on Galaxy A35; at
|
|
204
|
-
// 30 fps that's ~15 % CPU on flow alone). Evaluating every 5th
|
|
205
|
-
// frame yields novelty samples at ~6 Hz which is still well above
|
|
206
|
-
// the 1-2 Hz keyframe-accept cadence.
|
|
207
|
-
// matches pre-V16 behaviour). Set higher to cut CPU on long
|
|
208
|
-
// captures at the cost of acceptance latency.
|
|
209
|
-
flowEvalEveryNFrames: 5,
|
|
210
|
-
// V15.0c — sliver tweaks: leading-edge sliver from BOTTOM for typical
|
|
211
|
-
// top-to-bottom pan + full first-frame anchor produced the best
|
|
212
|
-
// outputs in early iteration.
|
|
213
|
-
sliverPosition: 'Bottom',
|
|
214
|
-
firstFrameFullFrame: true,
|
|
215
|
-
maxRecordingMs: 8000,
|
|
216
|
-
framesPerSecond: 3,
|
|
217
|
-
minFrames: 6,
|
|
218
|
-
maxFrames: 16,
|
|
219
|
-
quality: 85,
|
|
220
|
-
// 2026-05-14 (revised) — capture source defaults to 'ar' (AR-backed
|
|
221
|
-
// is the recommended path; non-AR is the explicit opt-out). Stitch
|
|
222
|
-
// mode stays 'auto' — the auto-resolution heuristic between PANORAMA
|
|
223
|
-
// and SCANS is per-capture, not per-mode, so it's safe to leave on.
|
|
224
|
-
captureSource: 'ar',
|
|
225
|
-
stitchMode: 'auto',
|
|
226
|
-
debug: false,
|
|
227
|
-
};
|
|
93
|
+
const PanoramaSettings_1 = require("./PanoramaSettings");
|
|
94
|
+
const lowMemDevice_1 = require("./lowMemDevice");
|
|
95
|
+
// ─── Device-memory diagnostic (informational only) ─────────────────
|
|
96
|
+
//
|
|
97
|
+
// Read once at module load via the shared `lowMemDevice` helper. We
|
|
98
|
+
// surface this as a single Menlo-monospace line at the top of the
|
|
99
|
+
// modal body so operators can see what the SDK detected — useful for
|
|
100
|
+
// diagnosing "why am I OOMing on this device?" questions. The same
|
|
101
|
+
// helper feeds <Camera>'s initial-settings device adaptation; they
|
|
102
|
+
// were duplicated implementations pre-Phase-2-fix.
|
|
103
|
+
const _physicalMemoryBytes = (0, lowMemDevice_1.getPhysicalMemoryBytes)();
|
|
104
|
+
const _isLowMem = (0, lowMemDevice_1.isLowMemDevice)();
|
|
228
105
|
function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
|
|
229
|
-
|
|
230
|
-
// V16 Phase 1b — derive the 2-axis (timing × algorithm) UI state
|
|
231
|
-
// from the underlying single `incrementalEngine` field. Storage
|
|
232
|
-
// shape is unchanged; the modal just presents it in two segmented
|
|
233
|
-
// controls so the user's mental model matches the system's actual
|
|
234
|
-
// primary axis (batch vs realtime).
|
|
106
|
+
// ─── Sub-tree update helpers ─────────────────────────────────────
|
|
235
107
|
//
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
//
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
//
|
|
258
|
-
|
|
108
|
+
// Each settings sub-tree has its own update helper that
|
|
109
|
+
// non-destructively patches that branch and re-emits the whole
|
|
110
|
+
// settings object via `onChange`. Call sites stay short
|
|
111
|
+
// (`updateStitcher({ stitchMode: 'scans' })`) and avoid the
|
|
112
|
+
// nested-spread boilerplate the hierarchical shape would otherwise
|
|
113
|
+
// require at every callsite.
|
|
114
|
+
//
|
|
115
|
+
// Why not a generic deep-merge? Type-safety: each helper takes
|
|
116
|
+
// exactly the `Partial<SubTree>` the section it backs can patch.
|
|
117
|
+
// A generic helper would accept arbitrary nested keys and break the
|
|
118
|
+
// type-level guarantee that the modal only mutates what its
|
|
119
|
+
// matching settings type defines.
|
|
120
|
+
const updateBase = (patch) => onChange({ ...settings, ...patch });
|
|
121
|
+
const updateStitcher = (patch) => onChange({
|
|
122
|
+
...settings,
|
|
123
|
+
stitcher: { ...settings.stitcher, ...patch },
|
|
124
|
+
});
|
|
125
|
+
const updateFrameSelection = (patch) => onChange({
|
|
126
|
+
...settings,
|
|
127
|
+
frameSelection: { ...settings.frameSelection, ...patch },
|
|
128
|
+
});
|
|
129
|
+
// Flow has an extra wrinkle: `frameSelection.flow` is optional.
|
|
130
|
+
// We materialise it from `DEFAULT_FLOW_GATE_SETTINGS` (the
|
|
131
|
+
// canonical FlowGateSettings defaults — see PanoramaSettings.ts)
|
|
132
|
+
// when patching from "undefined" — happens if a host starts with
|
|
133
|
+
// a custom settings literal that omits the sub-tree.
|
|
134
|
+
const updateFlow = (patch) => onChange({
|
|
135
|
+
...settings,
|
|
136
|
+
frameSelection: {
|
|
137
|
+
...settings.frameSelection,
|
|
138
|
+
flow: {
|
|
139
|
+
...(settings.frameSelection.flow ?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS),
|
|
140
|
+
...patch,
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
// Frame-selection mode controls the visibility of the nested
|
|
145
|
+
// Flow-tunables section. Mirrors the type-level optionality of
|
|
146
|
+
// `frameSelection.flow`.
|
|
147
|
+
const showFlowTunables = settings.frameSelection.mode === 'flow-based';
|
|
259
148
|
return (react_1.default.createElement(react_native_1.Modal, { visible: visible, animationType: "slide", transparent: true, statusBarTranslucent: true, onRequestClose: onClose },
|
|
260
149
|
react_1.default.createElement(react_native_1.View, { style: styles.backdrop },
|
|
261
150
|
react_1.default.createElement(react_native_1.View, { style: styles.sheet },
|
|
@@ -267,130 +156,84 @@ function PanoramaSettingsModal({ visible, settings, onChange, onClose, }) {
|
|
|
267
156
|
react_1.default.createElement(react_native_1.Text, { style: styles.debugLine }, `device: physicalMemoryBytes=${_physicalMemoryBytes} `
|
|
268
157
|
+ `(${(_physicalMemoryBytes / (1024 ** 3)).toFixed(2)} GB) · `
|
|
269
158
|
+ `isLowMem=${_isLowMem ? 'yes' : 'no'} · `
|
|
270
|
-
+ `
|
|
271
|
-
|
|
272
|
-
react_1.default.createElement(SegmentedControl, { options: ['ar', 'non-ar'], value: settings.captureSource, onChange: (v) => update({ captureSource: v }), caption: "ar (default): ARKit / ARCore \u2014 plane detection, pose-aware capture, full AR stack. non-ar: vision-camera only \u2014 disables AR plane detection, flips frame-selection to flow-based, runs IMU translation gate. In non-ar mode, the on-screen lens-switcher chip lets you toggle 0.5\u00D7 / 1\u00D7 lens during capture (only shown when the device has both lenses)." }),
|
|
273
|
-
react_1.default.createElement(SectionHeader, { title: "Stitch mode" }),
|
|
274
|
-
react_1.default.createElement(SegmentedControl, { options: ['auto', 'panorama', 'scans'], value: settings.stitchMode, onChange: (v) => update({ stitchMode: v }), caption: "auto (default): pick PANORAMA or SCANS based on translation/rotation totals at finalize. panorama: cv::Stitcher::PANORAMA \u2014 rotation-only (spherical warper, BA-ray); best for rotate-in-place captures, BAD on translation. scans: cv::Stitcher::SCANS \u2014 affine pipeline (plane warper, BA-affine); best for shelf-pan captures." }),
|
|
275
|
-
react_1.default.createElement(SectionHeader, { title: "Stitch timing" }),
|
|
276
|
-
react_1.default.createElement(SegmentedControl, { options: ['batch', 'realtime'], value: timing, onChange: (v) => setTiming(v), caption: "batch (recommended): full cv::Stitcher pipeline at shutter release. Highest quality. ~1\u20132 s post-release. realtime: incremental during pan; lower latency, fewer quality stages." }),
|
|
277
|
-
showFrameSelection && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
278
|
-
react_1.default.createElement(SectionHeader, { title: "Frame selection (V16)" }),
|
|
279
|
-
react_1.default.createElement(SegmentedControl, { options: ['time-based', 'pose-based', 'flow-based'], value: settings.frameSelectionMode, onChange: (v) => update({ frameSelectionMode: v }), caption: "flow-based (V16 A2, default): KeyframeGate uses sparse-Lucas-Kanade optical flow on full frame \u2014 plane-independent, invariant to plane size. pose-based: plane-polygon overlap (oversensitive on small latched planes). time-based: every ARFrame goes to the engine." }),
|
|
280
|
-
(settings.frameSelectionMode === 'pose-based' ||
|
|
281
|
-
settings.frameSelectionMode === 'flow-based') && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
282
|
-
react_1.default.createElement(SectionHeader, { title: "Overlap threshold (new content per keyframe)" }),
|
|
283
|
-
react_1.default.createElement(SegmentedControl, { options: ['20%', '30%', '40%', '50%', '60%'], value: `${Math.round(settings.keyframeOverlapThreshold * 100)}%`, onChange: (v) => update({ keyframeOverlapThreshold: parseInt(v, 10) / 100 }), caption: "Required NEW content per keyframe. 40% (default) \u2248 4\u20135 keyframes for a 90\u00B0 pan. Same threshold semantics for both pose-based and flow-based." }),
|
|
284
|
-
react_1.default.createElement(SectionHeader, { title: "Max keyframes per capture" }),
|
|
285
|
-
react_1.default.createElement(SegmentedControl, { options: ['3', '4', '5', '6', '8', '10'], value: String(settings.keyframeMaxCount), onChange: (v) => update({ keyframeMaxCount: parseInt(v, 10) }), caption: "Hard cap. 6 (default) matches Samsung's behaviour. Once reached, host auto-finalizes." }))),
|
|
286
|
-
settings.frameSelectionMode === 'flow-based' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
287
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 max corners" }),
|
|
288
|
-
react_1.default.createElement(SegmentedControl, { options: ['50', '100', '150', '200', '300'], value: String(settings.flowMaxCorners), onChange: (v) => update({ flowMaxCorners: parseInt(v, 10) }), caption: "Max Shi-Tomasi corners detected per accepted keyframe. More = more robust median, slower detect. 150 = default." }),
|
|
289
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 quality level" }),
|
|
290
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.005', '0.01', '0.02', '0.03', '0.05'], value: String(settings.flowQualityLevel), onChange: (v) => update({ flowQualityLevel: parseFloat(v) }), caption: "Shi-Tomasi corner quality threshold. Lower = more (weaker) corners; higher = fewer (stronger) corners. 0.01 = default." }),
|
|
291
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 min distance" }),
|
|
292
|
-
react_1.default.createElement(SegmentedControl, { options: ['5', '8', '10', '15', '20'], value: String(settings.flowMinDistance), onChange: (v) => update({ flowMinDistance: parseInt(v, 10) }), caption: "Min pixel distance between detected corners (working resolution = 720 px longest side). Higher = more spatially-spread features. 10 = default." }),
|
|
293
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 translation budget (cm)" }),
|
|
294
|
-
react_1.default.createElement(SegmentedControl, { options: ['0', '5', '8', '12', '20', '50'], value: String(settings.flowMaxTranslationCm), onChange: (v) => update({ flowMaxTranslationCm: parseInt(v, 10) }), caption: "Force-accept when camera has moved this many cm since last keyframe, even if novelty < overlap threshold. Bounds parallax so the stitcher can match. 0 = disabled (default). 8 = recommended starting value." }),
|
|
295
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 novelty percentile" }),
|
|
296
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.50', '0.70', '0.85', '0.95', '0.99'], value: settings.flowNoveltyPercentile.toFixed(2), onChange: (v) => update({ flowNoveltyPercentile: parseFloat(v) }), caption: "How tracked-feature displacements are aggregated into novelty. 0.50 = pre-V16 median behaviour (conservative). 0.85 = picks up leading-edge motion sooner (default, matches user perception). 0.99 = near-max, very aggressive." }),
|
|
297
|
-
react_1.default.createElement(SectionHeader, { title: "Flow tuning \u2014 eval every N frames" }),
|
|
298
|
-
react_1.default.createElement(SegmentedControl, { options: ['1', '2', '3', '5', '10'], value: String(settings.flowEvalEveryNFrames), onChange: (v) => update({ flowEvalEveryNFrames: parseInt(v, 10) }), caption: "Throttle gate evaluation to every Nth AR frame. 1 = every frame (default, no throttle). 3-5 = noticeable CPU/battery savings on long captures, up to N-1 frames of acceptance latency." }))))),
|
|
299
|
-
react_1.default.createElement(SectionHeader, { title: "AR plane projection" }),
|
|
300
|
-
react_1.default.createElement(SegmentedControl, { options: ['Disabled', 'ARKitDetected', 'Virtual'], value: settings.planeSource, onChange: (v) => update({ planeSource: v }), caption: "Disabled: no plane (gate falls back to angular delta). ARKitDetected: latch ARKit's vertical plane (best fidelity, picky). Virtual: synthesise plane perpendicular to camera at a fixed depth (always works)." }),
|
|
301
|
-
settings.planeSource === 'ARKitDetected' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
302
|
-
react_1.default.createElement(SectionHeader, { title: "ARKit alignment threshold" }),
|
|
303
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.3', '0.5', '0.6', '0.7', '0.85'], value: settings.arkitPlaneAlignmentThreshold.toFixed(2), onChange: (v) => update({ arkitPlaneAlignmentThreshold: parseFloat(v) }), caption: "Min dot product between candidate plane normal and camera facing. 0.6 (default) = ~53\u00B0 max angle off-camera. Higher = stricter." }))),
|
|
304
|
-
settings.planeSource === 'Virtual' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
305
|
-
react_1.default.createElement(SectionHeader, { title: "Virtual plane depth" }),
|
|
306
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.5m', '1.0m', '1.5m', '2.0m', '3.0m'], value: `${settings.virtualPlaneDepthMeters.toFixed(1)}m`, onChange: (v) => update({ virtualPlaneDepthMeters: parseFloat(v) }), caption: "Synthetic plane depth at first frame. Set to your typical scan distance." }))),
|
|
307
|
-
settings.planeSource !== 'Disabled' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
308
|
-
react_1.default.createElement(SectionHeader, { title: "Plane projection style" }),
|
|
309
|
-
react_1.default.createElement(SegmentedControl, { options: ['Rectified', 'Trapezoidal'], value: settings.planeProjectionStyle, onChange: (v) => update({ planeProjectionStyle: v }), caption: "Rectified (default): clean rectangle paste, no tilt distortion. Trapezoidal: V15.0b legacy 3D-correct raycast \u2014 geometric purity at the cost of tilt artifacts." }))),
|
|
310
|
-
react_1.default.createElement(SectionHeader, { title: "Algorithm" }),
|
|
311
|
-
timing === 'batch' ? (react_1.default.createElement(react_native_1.View, { style: styles.infoBox },
|
|
312
|
-
react_1.default.createElement(react_native_1.Text, { style: styles.infoText }, "Full feature-matched pipeline: ORB \u2192 BFMatcher \u2192 RANSAC \u2192 BundleAdjusterRay \u2192 waveCorrect \u2192 Warper \u2192 GraphCutSeamFinder \u2192 ExposureCompensator \u2192 MultiBandBlender. No engine choice in batch mode."))) : (react_1.default.createElement(SegmentedControl, { options: ['hybrid', 'slitscan-rotate', 'slitscan-both'], value: realtimeAlgorithm, onChange: (v) => update({ incrementalEngine: v }), caption: "hybrid: streaming planar projection + feature matching. slitscan-rotate: V13.0a + 1D NCC. slitscan-both: V13.0a + no accept gate + feather blend (iteration playground)." })),
|
|
313
|
-
timing === 'batch' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
314
|
-
react_1.default.createElement(SectionHeader, { title: "Batch tuning \u2014 Warper" }),
|
|
315
|
-
react_1.default.createElement(SegmentedControl, { options: ['plane', 'cylindrical', 'spherical'], value: settings.warperType, onChange: (v) => update({ warperType: v }), caption: "plane (default, recommended for retail shelves): flat rectangular output. cylindrical: rotational mid-arc, gentle curvature. spherical: wide pans (180\u00B0+) but always-curved." }),
|
|
316
|
-
react_1.default.createElement(SectionHeader, { title: "Batch tuning \u2014 Blender" }),
|
|
317
|
-
react_1.default.createElement(SegmentedControl, { options: ['multiband', 'feather'], value: settings.blenderType, onChange: (v) => update({ blenderType: v }), caption: "multiband (default): Laplacian-pyramid blending; cleanest seams. feather: faster, no halo when exposure varies." }),
|
|
318
|
-
react_1.default.createElement(SectionHeader, { title: "Batch tuning \u2014 Seam finder" }),
|
|
319
|
-
react_1.default.createElement(SegmentedControl, { options: ['graphcut', 'skip'], value: settings.seamFinderType, onChange: (v) => update({ seamFinderType: v }), caption: "graphcut (default): cv::detail::GraphCutSeamFinder; optimal seams, pairs with multiband, holds all warps in memory. skip: stream warp+feed (lower peak memory)." }),
|
|
320
|
-
react_1.default.createElement(SectionHeader, { title: "Batch tuning \u2014 Inscribed-rect crop" }),
|
|
321
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enableMaxInscribedRectCrop ? 'on' : 'off', onChange: (v) => update({ enableMaxInscribedRectCrop: v === 'on' }), caption: "off (default): final crop is just cv::boundingRect of non-black pixels \u2014 preserves all stitched content; may have black corners. on: additionally run MaxInscribedRectFromMask + column-projection second-pass for a clean-cornered rectangle \u2014 can shrink the output if the panorama mask is lopsided. A/B against the bbox crop on real scenes." }))),
|
|
322
|
-
timing === 'realtime' && realtimeAlgorithm === 'hybrid' && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
323
|
-
react_1.default.createElement(SectionHeader, { title: "Hybrid tuning \u2014 Projection" }),
|
|
324
|
-
react_1.default.createElement(SegmentedControl, { options: ['Planar', 'Cylindrical'], value: settings.hybridProjection, onChange: (v) => update({ hybridProjection: v }), caption: "Planar (default): cv::detail::PlaneWarper. Cylindrical: V12.x \u2013 V14.0a behaviour (legacy)." }))),
|
|
325
|
-
timing === 'realtime' && realtimeAlgorithm.startsWith('slitscan') && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
326
|
-
react_1.default.createElement(SectionHeader, { title: "Slit-scan tuning \u2014 Slit width" }),
|
|
327
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.01', '0.05', '0.10', '0.20', '0.30', '0.50'], value: settings.slitWidthFraction.toFixed(2), onChange: (v) => update({ slitWidthFraction: parseFloat(v) }), caption: "Fraction of pan-axis retained per sliver. 0.30 (V15 default) \u2248 324 px. Smaller = less within-slit depth disagreement." }),
|
|
328
|
-
react_1.default.createElement(SectionHeader, { title: "Slit-scan tuning \u2014 Sliver position" }),
|
|
329
|
-
react_1.default.createElement(SegmentedControl, { options: ['Center', 'Bottom', 'Top'], value: settings.sliverPosition, onChange: (v) => update({ sliverPosition: v }), caption: "Where on the camera sensor frame the sliver is taken." }),
|
|
330
|
-
react_1.default.createElement(SectionHeader, { title: "Slit-scan tuning \u2014 Full first-frame" }),
|
|
331
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.firstFrameFullFrame ? 'on' : 'off', onChange: (v) => update({ firstFrameFullFrame: v === 'on' }), caption: "ON: first accepted frame paints the full camera frame at the canvas anchor; subsequent frames use sliver clip." }),
|
|
332
|
-
react_1.default.createElement(SectionHeader, { title: "Slit-scan tuning \u2014 Paint mode" }),
|
|
333
|
-
react_1.default.createElement(SegmentedControl, { options: ['FirstPaintedWins', 'FeatherBlend'], value: settings.paintMode, onChange: (v) => update({ paintMode: v }), caption: "FirstPaintedWins (default): protect already-painted pixels. FeatherBlend: alpha-blend new content into overlap." }))),
|
|
334
|
-
react_1.default.createElement(Accordion, { title: "Advanced \u2014 2D NCC fine-alignment", badge: "advanced" },
|
|
335
|
-
react_1.default.createElement(SectionHeader, { title: "Enable 2D NCC" }),
|
|
336
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enable2dNcc ? 'on' : 'off', onChange: (v) => update({ enable2dNcc: v === 'on' }), caption: "V13.0g 2D NCC fine-alignment after pose-driven projection. Refines (\u0394x, \u0394y) translation via cv::matchTemplate." }),
|
|
337
|
-
settings.enable2dNcc && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
338
|
-
react_1.default.createElement(SectionHeader, { title: "Confidence threshold" }),
|
|
339
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.50', '0.65', '0.75', '0.85', '0.95', '0.99'], value: settings.nccConfidenceThreshold2d.toFixed(2), onChange: (v) => update({ nccConfidenceThreshold2d: parseFloat(v) }), caption: "Reject NCC corrections below this confidence. 0.99 = only apply on near-perfect overlap." }),
|
|
340
|
-
react_1.default.createElement(SectionHeader, { title: "Search half-window (px)" }),
|
|
341
|
-
react_1.default.createElement(SegmentedControl, { options: ['6', '10', '12', '20', '30'], value: String(settings.nccSearchMargin2d), onChange: (v) => update({ nccSearchMargin2d: parseInt(v, 10) }), caption: "Pixels: 2D NCC searches \u00B1this around the pose-predicted match." }),
|
|
342
|
-
react_1.default.createElement(SectionHeader, { title: "EMA smoothing" }),
|
|
343
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enableNcc2dEmaSmoothing ? 'on' : 'off', onChange: (v) => update({ enableNcc2dEmaSmoothing: v === 'on' }), caption: "Damp single-frame snaps to spurious peaks via EMA." }),
|
|
344
|
-
settings.enableNcc2dEmaSmoothing && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
345
|
-
react_1.default.createElement(SectionHeader, { title: "EMA alpha (current-frame weight)" }),
|
|
346
|
-
react_1.default.createElement(SegmentedControl, { options: ['0.20', '0.30', '0.40', '0.60', '0.80'], value: settings.ncc2dEmaAlpha.toFixed(2), onChange: (v) => update({ ncc2dEmaAlpha: parseFloat(v) }) }))),
|
|
347
|
-
react_1.default.createElement(SectionHeader, { title: "Pan-axis lock" }),
|
|
348
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enableNcc2dPanAxisLock ? 'on' : 'off', onChange: (v) => update({ enableNcc2dPanAxisLock: v === 'on' }), caption: "Clamp cross-axis correction tighter than pan-axis (pose + 1D NCC handle cross-axis already)." }),
|
|
349
|
-
settings.enableNcc2dPanAxisLock && (react_1.default.createElement(react_1.default.Fragment, null,
|
|
350
|
-
react_1.default.createElement(SectionHeader, { title: "Cross-axis clamp (px)" }),
|
|
351
|
-
react_1.default.createElement(SegmentedControl, { options: ['2', '5', '10', '15'], value: String(settings.ncc2dCrossAxisLockPx), onChange: (v) => update({ ncc2dCrossAxisLockPx: parseInt(v, 10) }) })))))),
|
|
352
|
-
timing === 'realtime' && realtimeAlgorithm === 'slitscan-both' && (react_1.default.createElement(Accordion, { title: "Advanced \u2014 Slit-scan experimental", badge: "experimental" },
|
|
353
|
-
react_1.default.createElement(SectionHeader, { title: "Triangulation parallax" }),
|
|
354
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enableTriangulation ? 'on' : 'off', onChange: (v) => update({ enableTriangulation: v === 'on' }), caption: "V13.0e ORB triangulation + median-Z parallax correction. Adds ~10ms/accept." }),
|
|
355
|
-
react_1.default.createElement(SectionHeader, { title: "RANSAC homography" }),
|
|
356
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.enableRansacHomography ? 'on' : 'off', onChange: (v) => update({ enableRansacHomography: v === 'on' }), caption: "V14.0a RANSAC homography per slit + cv::warpPerspective. Known limitation: can absorb pan as scale, leaving gaps." }),
|
|
357
|
-
react_1.default.createElement(SectionHeader, { title: "Accept gate (px)" }),
|
|
358
|
-
react_1.default.createElement(SegmentedControl, { options: ['0', '50'], value: String(settings.acceptGate), onChange: (v) => update({ acceptGate: parseInt(v, 10) }), caption: "0 = accept on every frame (Apple-dense). 50 = V13.0g throttle." }))),
|
|
359
|
-
react_1.default.createElement(SectionHeader, { title: "Recording cap" }),
|
|
360
|
-
react_1.default.createElement(SegmentedControl, { options: ['4 s', '6 s', '8 s', '10 s'], value: `${Math.round(settings.maxRecordingMs / 1000)} s`, onChange: (v) => update({ maxRecordingMs: parseInt(v, 10) * 1000 }), caption: "Auto-stops the hold-recording at this duration." }),
|
|
361
|
-
react_1.default.createElement(SectionHeader, { title: "JPEG quality" }),
|
|
362
|
-
react_1.default.createElement(SegmentedControl, { options: ['70', '85', '92'], value: String(settings.quality), onChange: (v) => update({ quality: parseInt(v, 10) }), caption: "Higher = bigger files, sharper detail. 85 is the recommended default." }),
|
|
159
|
+
+ `current blender=${settings.stitcher.blenderType} `
|
|
160
|
+
+ `(low-mem fallback=${_isLowMem ? 'feather' : 'multiband'})`),
|
|
363
161
|
react_1.default.createElement(SectionHeader, { title: "Debug" }),
|
|
364
|
-
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.debug ? 'on' : 'off', onChange: (v) =>
|
|
365
|
-
react_1.default.createElement(Accordion, { title: "
|
|
366
|
-
react_1.default.createElement(
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
162
|
+
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.debug ? 'on' : 'off', onChange: (v) => updateBase({ debug: v === 'on' }), caption: "When ON, <Camera> mounts the diagnostic pills (memory, keyframes, orientation), the detailed metrics overlay, and the stitch-stats toast on every successful finalize. OFF (default) \u2014 production end-user UI." }),
|
|
163
|
+
react_1.default.createElement(Accordion, { title: "Frame selection (KeyframeGate)" },
|
|
164
|
+
react_1.default.createElement(SectionHeader, { title: "Mode" }),
|
|
165
|
+
react_1.default.createElement(SegmentedControl, { options: ['time-based', 'pose-based', 'flow-based'], value: settings.frameSelection.mode, onChange: (v) => updateFrameSelection({
|
|
166
|
+
mode: v,
|
|
167
|
+
}), caption: "flow-based (default): sparse Shi-Tomasi + KLT optical flow. Plane-independent. pose-based: plane-overlap when a plane is latched, angular fallback otherwise \u2014 cheap but conservative. time-based: gate disabled; every frame accepted up to maxKeyframes." }),
|
|
168
|
+
react_1.default.createElement(SectionHeader, { title: "Max keyframes per capture" }),
|
|
169
|
+
react_1.default.createElement(SegmentedControl, { options: ['3', '4', '5', '6', '8', '10'], value: String(settings.frameSelection.maxKeyframes), onChange: (v) => updateFrameSelection({
|
|
170
|
+
maxKeyframes: parseInt(v, 10),
|
|
171
|
+
}), caption: "Hard cap on accepted keyframes; native clamps to [3, 10]. 6 (default) matches Samsung Pano's behaviour and is the sweet spot for cv::Stitcher BA convergence." }),
|
|
172
|
+
react_1.default.createElement(SectionHeader, { title: "Overlap threshold (new content per keyframe)" }),
|
|
173
|
+
react_1.default.createElement(SegmentedControl, { options: ['20%', '30%', '40%', '50%', '60%'], value: `${Math.round(settings.frameSelection.overlapThreshold * 100)}%`, onChange: (v) => updateFrameSelection({
|
|
174
|
+
overlapThreshold: parseInt(v, 10) / 100,
|
|
175
|
+
}), caption: "Required NEW-content fraction. 20% (default): generous, ~5\u20136 keyframes for a 90\u00B0 pan. Native clamps to [10%, 80%]." }),
|
|
176
|
+
showFlowTunables && (react_1.default.createElement(react_native_1.View, { style: styles.nested },
|
|
177
|
+
react_1.default.createElement(react_native_1.Text, { style: styles.nestedLabel }, "Flow tuning"),
|
|
178
|
+
react_1.default.createElement(SectionHeader, { title: "Max corners (Shi-Tomasi)" }),
|
|
179
|
+
react_1.default.createElement(SegmentedControl, { options: ['50', '100', '150', '200', '300'], value: String(settings.frameSelection.flow?.maxCorners
|
|
180
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.maxCorners), onChange: (v) => updateFlow({ maxCorners: parseInt(v, 10) }), caption: "More corners = more robust median displacement, slower detect. 150 (default) ~ 15\u201325 ms / frame on Galaxy A35. Native clamps to [50, 300]." }),
|
|
181
|
+
react_1.default.createElement(SectionHeader, { title: "Quality level (Shi-Tomasi)" }),
|
|
182
|
+
react_1.default.createElement(SegmentedControl, { options: ['0.005', '0.01', '0.02', '0.03', '0.05'], value: String(settings.frameSelection.flow?.qualityLevel
|
|
183
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.qualityLevel), onChange: (v) => updateFlow({ qualityLevel: parseFloat(v) }), caption: "Lower lets weaker corners in; higher demands stronger corners. 0.01 (default). Clamped to [0.005, 0.05]." }),
|
|
184
|
+
react_1.default.createElement(SectionHeader, { title: "Min distance (working-resolution px)" }),
|
|
185
|
+
react_1.default.createElement(SegmentedControl, { options: ['5', '8', '10', '15', '20'], value: String(settings.frameSelection.flow?.minDistance
|
|
186
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.minDistance), onChange: (v) => updateFlow({ minDistance: parseInt(v, 10) }), caption: "Min pixel distance between detected corners (working res = 720 px longest side). 10 (default). Clamped to [1, 50]." }),
|
|
187
|
+
react_1.default.createElement(SectionHeader, { title: "Translation budget (cm)" }),
|
|
188
|
+
react_1.default.createElement(SegmentedControl, { options: ['0', '5', '8', '12', '20', '50'], value: String(settings.frameSelection.flow?.maxTranslationCm
|
|
189
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.maxTranslationCm), onChange: (v) => updateFlow({
|
|
190
|
+
maxTranslationCm: parseInt(v, 10),
|
|
191
|
+
}), caption: "Force-accept the next frame once the operator has translated this many cm since the last keyframe, even when novelty < threshold. Bounds parallax so cv::Stitcher's matcher can handle the input. 50 (default). 0 disables. Clamped to [0, 100]." }),
|
|
192
|
+
react_1.default.createElement(SectionHeader, { title: "Novelty percentile" }),
|
|
193
|
+
react_1.default.createElement(SegmentedControl, { options: ['0.50', '0.70', '0.85', '0.95', '0.99'], value: (settings.frameSelection.flow?.noveltyPercentile
|
|
194
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.noveltyPercentile).toFixed(2), onChange: (v) => updateFlow({
|
|
195
|
+
noveltyPercentile: parseFloat(v),
|
|
196
|
+
}), caption: "How tracked-feature displacements aggregate into a per-axis novelty estimate. 0.85 (default): picks up leading-edge motion sooner \u2014 matches user perception. 0.50: pre-V16 median (conservative). 0.99: very aggressive. Clamped to [0.50, 0.99]." }),
|
|
197
|
+
react_1.default.createElement(SectionHeader, { title: "Eval every N frames" }),
|
|
198
|
+
react_1.default.createElement(SegmentedControl, { options: ['1', '2', '3', '5', '10'], value: String(settings.frameSelection.flow?.evalEveryNFrames
|
|
199
|
+
?? PanoramaSettings_1.DEFAULT_FLOW_GATE_SETTINGS.evalEveryNFrames), onChange: (v) => updateFlow({
|
|
200
|
+
evalEveryNFrames: parseInt(v, 10),
|
|
201
|
+
}), caption: "Throttle gate evaluation to every Nth frame for CPU savings. 5 (default) gives ~6 Hz novelty samples at 30 Hz ARCore. Doesn't change WHICH frames are accepted; only the sample rate. Clamped to [1, 10]." })))),
|
|
202
|
+
react_1.default.createElement(Accordion, { title: "Stitcher (cv::Stitcher knobs)" },
|
|
203
|
+
react_1.default.createElement(SectionHeader, { title: "Stitch mode" }),
|
|
204
|
+
react_1.default.createElement(SegmentedControl, { options: ['auto', 'panorama', 'scans'], value: settings.stitcher.stitchMode, onChange: (v) => updateStitcher({
|
|
205
|
+
stitchMode: v,
|
|
206
|
+
}), caption: "auto (default): pick PANORAMA or SCANS based on translation/rotation totals at finalize. panorama: rotation-only (spherical warper, BA-Ray) \u2014 best for rotate-in-place captures; BAD on translation. scans: affine pipeline (plane warper, BA-affine) \u2014 best for shelf-pan captures; never diverges on rotation either. Both modes auto-retry with the opposite if camera params come out degenerate." }),
|
|
207
|
+
react_1.default.createElement(SectionHeader, { title: "Warper" }),
|
|
208
|
+
react_1.default.createElement(SegmentedControl, { options: ['plane', 'cylindrical', 'spherical'], value: settings.stitcher.warperType, onChange: (v) => updateStitcher({
|
|
209
|
+
warperType: v,
|
|
210
|
+
}), caption: "plane (default): flat rectangular output, best for retail shelves. cylindrical: rotational mid-arc. spherical: wide pans (180\u00B0+), always curved. Only consulted in panorama mode; scans hardwires PlaneWarper." }),
|
|
211
|
+
react_1.default.createElement(SectionHeader, { title: "Blender" }),
|
|
212
|
+
react_1.default.createElement(SegmentedControl, { options: ['multiband', 'feather'], value: settings.stitcher.blenderType, onChange: (v) => updateStitcher({
|
|
213
|
+
blenderType: v,
|
|
214
|
+
}), caption: "multiband (default): Laplacian-pyramid blending; cleanest seams, holds all warped frames in memory. feather: streams warp+feed (lower peak memory, no halo with varied exposure). <Camera> auto-picks feather on low-memory devices." }),
|
|
215
|
+
react_1.default.createElement(SectionHeader, { title: "Seam finder" }),
|
|
216
|
+
react_1.default.createElement(SegmentedControl, { options: ['graphcut', 'skip'], value: settings.stitcher.seamFinderType, onChange: (v) => updateStitcher({
|
|
217
|
+
seamFinderType: v,
|
|
218
|
+
}), caption: "graphcut (default): cv::detail::GraphCutSeamFinder for optimal seams; pairs with multiband. skip: stream warp+feed (lowest-memory configuration; pair with feather)." }),
|
|
219
|
+
react_1.default.createElement(SectionHeader, { title: "Inscribed-rect crop" }),
|
|
220
|
+
react_1.default.createElement(SegmentedControl, { options: ['off', 'on'], value: settings.stitcher.enableMaxInscribedRectCrop ? 'on' : 'off', onChange: (v) => updateStitcher({
|
|
221
|
+
enableMaxInscribedRectCrop: v === 'on',
|
|
222
|
+
}), caption: "off (default): crop to cv::boundingRect of non-black pixels \u2014 preserves all stitched content; may leave black corners. on: run MaxInscribedRectFromMask + column-projection second-pass for a clean rectangle (can shrink output if mask is lopsided)." })),
|
|
223
|
+
react_1.default.createElement(react_native_1.Pressable, { onPress: () => onChange(PanoramaSettings_1.DEFAULT_PANORAMA_SETTINGS), style: styles.resetBtn, accessibilityRole: "button", accessibilityLabel: "Reset to defaults" },
|
|
379
224
|
react_1.default.createElement(react_native_1.Text, { style: styles.resetText }, "Reset to defaults")))))));
|
|
380
225
|
}
|
|
226
|
+
// ════════════════════════════════════════════════════════════════════
|
|
227
|
+
// Helpers (kept verbatim from v0.3 — presentational primitives the
|
|
228
|
+
// modal composes; nothing in here depends on the settings shape).
|
|
229
|
+
// ════════════════════════════════════════════════════════════════════
|
|
381
230
|
function SectionHeader({ title }) {
|
|
382
231
|
return react_1.default.createElement(react_native_1.Text, { style: styles.sectionHeader }, title);
|
|
383
232
|
}
|
|
384
233
|
/**
|
|
385
|
-
* Collapsible section.
|
|
386
|
-
*
|
|
387
|
-
*
|
|
388
|
-
* day-to-day.
|
|
389
|
-
*
|
|
390
|
-
* State is local — each Accordion instance manages its own open
|
|
391
|
-
* flag. The modal opens fresh-collapsed every mount which is what
|
|
392
|
-
* we want for now; persisting open state across mounts (e.g. via
|
|
393
|
-
* AsyncStorage) is a future enhancement.
|
|
234
|
+
* Collapsible section. Each instance owns its open/closed state;
|
|
235
|
+
* the modal opens fresh-collapsed on every mount, which is what we
|
|
236
|
+
* want (no AsyncStorage roundtrip on every settings tweak).
|
|
394
237
|
*/
|
|
395
238
|
function Accordion({ title, initiallyOpen = false, badge, children, }) {
|
|
396
239
|
const [open, setOpen] = (0, react_1.useState)(initiallyOpen);
|
|
@@ -402,9 +245,10 @@ function Accordion({ title, initiallyOpen = false, badge, children, }) {
|
|
|
402
245
|
open ? react_1.default.createElement(react_native_1.View, { style: styles.accordionBody }, children) : null));
|
|
403
246
|
}
|
|
404
247
|
/**
|
|
405
|
-
* Small grey-text badge
|
|
406
|
-
* "experimental",
|
|
407
|
-
*
|
|
248
|
+
* Small grey-text badge — marks sections as "advanced",
|
|
249
|
+
* "experimental", or similar. No semantic effect; purely a quick
|
|
250
|
+
* visual signal. Kept for future Layer-2 settings modals that may
|
|
251
|
+
* want to flag experimental sub-trees.
|
|
408
252
|
*/
|
|
409
253
|
function Tag({ label }) {
|
|
410
254
|
return (react_1.default.createElement(react_native_1.View, { style: styles.tag },
|
|
@@ -480,18 +324,21 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
480
324
|
marginTop: 18,
|
|
481
325
|
marginBottom: 8,
|
|
482
326
|
},
|
|
483
|
-
|
|
484
|
-
|
|
327
|
+
// Nested-sub-section label inside an accordion body — used for the
|
|
328
|
+
// Flow-tunables sub-tree under Frame selection.
|
|
329
|
+
nested: {
|
|
330
|
+
marginTop: 12,
|
|
331
|
+
paddingTop: 12,
|
|
332
|
+
borderTopWidth: react_native_1.StyleSheet.hairlineWidth,
|
|
333
|
+
borderTopColor: 'rgba(255,255,255,0.08)',
|
|
485
334
|
},
|
|
486
|
-
|
|
487
|
-
color: '
|
|
488
|
-
|
|
489
|
-
fontSize: 13,
|
|
335
|
+
nestedLabel: {
|
|
336
|
+
color: 'rgba(255,255,255,0.55)',
|
|
337
|
+
fontSize: 11,
|
|
490
338
|
fontWeight: '600',
|
|
491
339
|
textTransform: 'uppercase',
|
|
492
|
-
letterSpacing: 0.
|
|
493
|
-
|
|
494
|
-
marginBottom: 8,
|
|
340
|
+
letterSpacing: 0.8,
|
|
341
|
+
marginBottom: 4,
|
|
495
342
|
},
|
|
496
343
|
segmentedRow: {
|
|
497
344
|
flexDirection: 'row',
|
|
@@ -551,7 +398,6 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
551
398
|
fontSize: 14,
|
|
552
399
|
fontWeight: '500',
|
|
553
400
|
},
|
|
554
|
-
// V16 Phase 1b — Accordion + Tag + InfoBox
|
|
555
401
|
accordion: {
|
|
556
402
|
marginTop: 18,
|
|
557
403
|
backgroundColor: 'rgba(255,255,255,0.04)',
|
|
@@ -596,16 +442,5 @@ const styles = react_native_1.StyleSheet.create({
|
|
|
596
442
|
textTransform: 'uppercase',
|
|
597
443
|
letterSpacing: 0.5,
|
|
598
444
|
},
|
|
599
|
-
infoBox: {
|
|
600
|
-
backgroundColor: 'rgba(255,255,255,0.06)',
|
|
601
|
-
borderRadius: 8,
|
|
602
|
-
padding: 12,
|
|
603
|
-
marginTop: 4,
|
|
604
|
-
},
|
|
605
|
-
infoText: {
|
|
606
|
-
color: 'rgba(255,255,255,0.75)',
|
|
607
|
-
fontSize: 12,
|
|
608
|
-
lineHeight: 17,
|
|
609
|
-
},
|
|
610
445
|
});
|
|
611
446
|
//# sourceMappingURL=PanoramaSettingsModal.js.map
|