react-native-image-stitcher 0.1.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ /**
4
+ * PanoramaGuidance — gyroscope-driven pan-speed indicator for the
5
+ * tap-and-hold panorama flow.
6
+ *
7
+ * ┌──────────────────────────────────────────────────────────┐
8
+ * │ (camera preview) │
9
+ * │ │
10
+ * │ ↓ │ ← portrait + landscape pan
11
+ * │ green / yellow / red │
12
+ * │ │
13
+ * │ "Pan slowly" / "Slow down" / "Too fast" │
14
+ * └──────────────────────────────────────────────────────────┘
15
+ *
16
+ * Why this exists
17
+ * The SCANS-mode stitcher needs ~30–50 % overlap between
18
+ * consecutive frames. At 30 fps, frames are ~33 ms apart, so
19
+ * pan rates above roughly 30°/s (≈ 0.5 rad/s) produce frames
20
+ * the stitcher can't align — and the user finds out only after
21
+ * the post-release "Stitching failed" alert. Real-time feedback
22
+ * prevents that failure mode.
23
+ *
24
+ * What it does
25
+ * - Subscribes to the device gyroscope (react-native-sensors)
26
+ * ONLY while `active` is true; tears down on inactive so the
27
+ * sensor isn't running the rest of the time the screen is up.
28
+ * - Detects portrait vs landscape from window dimensions; the
29
+ * dominant pan axis changes accordingly:
30
+ * portrait → user pans horizontally → we track gyro Y.
31
+ * landscape → user pans vertically → we track gyro X.
32
+ * - Maps the dominant axis's |rad/s| onto a colour scale and a
33
+ * human-readable hint. Defaults are tuned for SCANS but
34
+ * overrideable.
35
+ *
36
+ * Performance
37
+ * The gyroscope fires ~30 Hz. We update an Animated.Value (which
38
+ * updates the colour interpolation on the native driver) and only
39
+ * call setState when the qualitative bucket (good/warn/bad)
40
+ * changes — keeps re-render volume low.
41
+ */
42
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ var desc = Object.getOwnPropertyDescriptor(m, k);
45
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
46
+ desc = { enumerable: true, get: function() { return m[k]; } };
47
+ }
48
+ Object.defineProperty(o, k2, desc);
49
+ }) : (function(o, m, k, k2) {
50
+ if (k2 === undefined) k2 = k;
51
+ o[k2] = m[k];
52
+ }));
53
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
54
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
55
+ }) : function(o, v) {
56
+ o["default"] = v;
57
+ });
58
+ var __importStar = (this && this.__importStar) || (function () {
59
+ var ownKeys = function(o) {
60
+ ownKeys = Object.getOwnPropertyNames || function (o) {
61
+ var ar = [];
62
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
63
+ return ar;
64
+ };
65
+ return ownKeys(o);
66
+ };
67
+ return function (mod) {
68
+ if (mod && mod.__esModule) return mod;
69
+ var result = {};
70
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
71
+ __setModuleDefault(result, mod);
72
+ return result;
73
+ };
74
+ })();
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.PanoramaGuidance = PanoramaGuidance;
77
+ const react_1 = __importStar(require("react"));
78
+ const react_native_1 = require("react-native");
79
+ const react_native_sensors_1 = require("react-native-sensors");
80
+ const useDeviceOrientation_1 = require("./useDeviceOrientation");
81
+ const DEFAULT_GOOD = 0.5;
82
+ const DEFAULT_WARN = 1.0;
83
+ const COLOR_GOOD = '#34C759';
84
+ const COLOR_WARN = '#FFCC00';
85
+ const COLOR_BAD = '#FF3B30';
86
+ const DEFAULT_MESSAGES = {
87
+ good: 'Good pace — keep going',
88
+ warn: 'Slow down a bit',
89
+ bad: 'Too fast — slow down',
90
+ };
91
+ function bucketFor(rate, good, warn) {
92
+ const abs = Math.abs(rate);
93
+ if (abs <= good)
94
+ return 'good';
95
+ if (abs <= warn)
96
+ return 'warn';
97
+ return 'bad';
98
+ }
99
+ function colorFor(bucket) {
100
+ switch (bucket) {
101
+ case 'good':
102
+ return COLOR_GOOD;
103
+ case 'warn':
104
+ return COLOR_WARN;
105
+ case 'bad':
106
+ return COLOR_BAD;
107
+ }
108
+ }
109
+ function PanoramaGuidance({ active, axis, goodMaxRadPerSec = DEFAULT_GOOD, warnMaxRadPerSec = DEFAULT_WARN, messages, style, }) {
110
+ // Use the accelerometer-based hook (NOT useWindowDimensions) so
111
+ // we detect physical orientation even though the app is
112
+ // portrait-locked at the OS level.
113
+ const deviceOrientation = (0, useDeviceOrientation_1.useDeviceOrientation)();
114
+ const isPortrait = deviceOrientation === 'portrait'
115
+ || deviceOrientation === 'portrait-upside-down';
116
+ // Auto-detect: in portrait the user pans horizontally
117
+ // (left↔right across the rack) → gyro Y axis dominates.
118
+ // In landscape the user pans vertically (up↕down a tall fixture)
119
+ // → gyro X axis dominates.
120
+ const resolvedAxis = axis ?? (isPortrait ? 'horizontal' : 'vertical');
121
+ // Qualitative bucket — drives both the message and (via colour)
122
+ // the arrow tint. Stored in state so a *change* in bucket
123
+ // re-renders, but per-sample updates do NOT.
124
+ const [bucket, setBucket] = (0, react_1.useState)('good');
125
+ // Last known rotation rate for the dominant axis, kept in a ref
126
+ // to avoid re-rendering every sample. Read by the bucket logic
127
+ // and (indirectly, via colour interpolation) the animated tint.
128
+ const lastBucketRef = (0, react_1.useRef)('good');
129
+ (0, react_1.useEffect)(() => {
130
+ if (!active) {
131
+ lastBucketRef.current = 'good';
132
+ setBucket('good');
133
+ return;
134
+ }
135
+ // Sample at 33 ms (~30 Hz) — matches the typical recording
136
+ // frame rate so each gyro sample maps to one frame's pan.
137
+ (0, react_native_sensors_1.setUpdateIntervalForType)(react_native_sensors_1.SensorTypes.gyroscope, 33);
138
+ let subscription = react_native_sensors_1.gyroscope.subscribe({
139
+ next: ({ x, y }) => {
140
+ const rate = resolvedAxis === 'horizontal' ? y : x;
141
+ const next = bucketFor(rate, goodMaxRadPerSec, warnMaxRadPerSec);
142
+ if (next !== lastBucketRef.current) {
143
+ lastBucketRef.current = next;
144
+ setBucket(next);
145
+ }
146
+ },
147
+ error: (err) => {
148
+ // eslint-disable-next-line no-console
149
+ console.warn('[PanoramaGuidance] gyroscope error', err);
150
+ },
151
+ });
152
+ return () => {
153
+ subscription?.unsubscribe();
154
+ subscription = null;
155
+ };
156
+ }, [active, resolvedAxis, goodMaxRadPerSec, warnMaxRadPerSec]);
157
+ const resolvedMessages = (0, react_1.useMemo)(() => ({
158
+ good: messages?.good ?? DEFAULT_MESSAGES.good,
159
+ warn: messages?.warn ?? DEFAULT_MESSAGES.warn,
160
+ bad: messages?.bad ?? DEFAULT_MESSAGES.bad,
161
+ }), [messages]);
162
+ if (!active)
163
+ return null;
164
+ const tint = colorFor(bucket);
165
+ const message = resolvedMessages[bucket];
166
+ // Arrow glyph for the dominant axis. The arrow renders a
167
+ // pannable direction hint — landscape gets a vertical arrow
168
+ // (the user's panning that way), portrait gets a horizontal
169
+ // arrow.
170
+ const arrow = resolvedAxis === 'horizontal' ? '↔' : '↕';
171
+ // Place the pill at user-perceived bottom across all four
172
+ // orientations. Same pattern as <CaptureStatusOverlay> — the
173
+ // app layout is portrait-locked so we re-position via absolute
174
+ // coords + apply a rotation transform.
175
+ const pillOrientationStyle = pillStyleForOrientation(deviceOrientation);
176
+ return (react_1.default.createElement(react_native_1.View, { pointerEvents: "none", style: [styles.root, pillOrientationStyle, style], accessibilityRole: "alert", accessibilityLiveRegion: "polite" },
177
+ react_1.default.createElement(react_native_1.View, { style: [styles.pill, { borderColor: tint }] },
178
+ react_1.default.createElement(react_native_1.Text, { style: [styles.arrow, { color: tint }] }, arrow),
179
+ react_1.default.createElement(react_native_1.Text, { style: [styles.message, { color: tint }], numberOfLines: 1 }, message))));
180
+ }
181
+ /**
182
+ * Mirror of bannerStyleForOrientation in CaptureStatusOverlay,
183
+ * but anchored at user-perceived BOTTOM instead of TOP.
184
+ */
185
+ function pillStyleForOrientation(orientation) {
186
+ switch (orientation) {
187
+ case 'landscape-left':
188
+ return {
189
+ top: 0,
190
+ bottom: 0,
191
+ left: 8,
192
+ alignItems: 'flex-start',
193
+ justifyContent: 'center',
194
+ transform: [{ rotate: '90deg' }],
195
+ };
196
+ case 'landscape-right':
197
+ return {
198
+ top: 0,
199
+ bottom: 0,
200
+ right: 8,
201
+ alignItems: 'flex-end',
202
+ justifyContent: 'center',
203
+ transform: [{ rotate: '-90deg' }],
204
+ };
205
+ case 'portrait-upside-down':
206
+ return {
207
+ top: 24,
208
+ left: 0,
209
+ right: 0,
210
+ alignItems: 'center',
211
+ transform: [{ rotate: '180deg' }],
212
+ };
213
+ case 'portrait':
214
+ default:
215
+ return {
216
+ bottom: 24,
217
+ left: 0,
218
+ right: 0,
219
+ alignItems: 'center',
220
+ };
221
+ }
222
+ }
223
+ const styles = react_native_1.StyleSheet.create({
224
+ root: {
225
+ position: 'absolute',
226
+ },
227
+ pill: {
228
+ flexDirection: 'row',
229
+ alignItems: 'center',
230
+ paddingHorizontal: 14,
231
+ paddingVertical: 8,
232
+ borderRadius: 24,
233
+ borderWidth: 2,
234
+ backgroundColor: 'rgba(0,0,0,0.55)',
235
+ },
236
+ arrow: {
237
+ fontSize: 22,
238
+ fontWeight: '700',
239
+ marginRight: 8,
240
+ },
241
+ message: {
242
+ fontSize: 14,
243
+ fontWeight: '600',
244
+ },
245
+ });
246
+ //# sourceMappingURL=PanoramaGuidance.js.map
@@ -0,0 +1,311 @@
1
+ /**
2
+ * PanoramaSettingsModal — runtime A/B testing surface for the
3
+ * stitcher pipeline. Operators in the field can toggle warper,
4
+ * blender, and tuning constants between captures to see what
5
+ * looks best on real shelf scenes.
6
+ *
7
+ * The modal is presentational: the host owns the settings state
8
+ * (typically `useState<PanoramaSettings>`) and renders the modal
9
+ * with `visible` toggled by a gear-icon press in the capture
10
+ * header. Settings flow OUT via `onChange` for each tweak.
11
+ *
12
+ * Why expose this as an SDK component instead of leaving it to
13
+ * each host? The set of tunable knobs IS the SDK's contract —
14
+ * if a new setting is added (e.g. registration MP) the SDK ships
15
+ * the UI for it in lockstep with the param itself, instead of
16
+ * forcing every host app to update its settings screen.
17
+ */
18
+ import React from 'react';
19
+ export interface PanoramaSettings {
20
+ warperType: 'plane' | 'cylindrical' | 'spherical';
21
+ blenderType: 'multiband' | 'feather';
22
+ /**
23
+ * Seam finder strategy. "graphcut" finds optimal seams before
24
+ * blending (cleaner output, pairs with multiband, more memory).
25
+ * "skip" streams warp+feed (lower peak memory, fine with feather).
26
+ */
27
+ seamFinderType: 'graphcut' | 'skip';
28
+ /**
29
+ * V16 Phase 1b.fix5c (Ram's call 2026-05-10) — toggle the
30
+ * max-inscribed-rectangle crop on the batch-keyframe output
31
+ * panorama. When false (default), the output is cropped to the
32
+ * bounding rectangle of non-black pixels only (cv::boundingRect)
33
+ * — preserves all stitched content at the cost of some black
34
+ * corners where cv::Stitcher's projection didn't fill. When
35
+ * true, the post-stitch pipeline additionally runs
36
+ * `MaxInscribedRectFromMask` to find the largest axis-aligned
37
+ * rectangle entirely inside content, followed by the
38
+ * column-projection second-pass. Inscribed-rect can be
39
+ * over-aggressive on lopsided masks (field log showed a
40
+ * 1146×1102 bbox shrinking to a 602×1102 strip), so default OFF
41
+ * lets the operator see the full stitched scene; flip ON to
42
+ * A/B against the cleaner-but-smaller output.
43
+ */
44
+ enableMaxInscribedRectCrop: boolean;
45
+ /**
46
+ * Phase 4.4 EXPERIMENTAL: when true, the host swaps the
47
+ * vision-camera-backed CameraView for an ARKit-backed ARCameraView
48
+ * during panorama capture. Default false (keeps the existing
49
+ * stitcher flow untouched). Phase 5 will add AR-backed photo /
50
+ * video capture and pose-driven stitching; until then this is
51
+ * preview-only — useful for verifying the AR session renders
52
+ * cleanly on the operator's device before we cut over.
53
+ */
54
+ useARPreview: boolean;
55
+ /**
56
+ * V15 — Incremental engine choice for live realtime stitching.
57
+ * 'hybrid' — Whole-frame projection + feature matching;
58
+ * planar by default (was cylindrical).
59
+ * 'slitscan-rotate' — V13.0a baseline + 1D NCC for rotation
60
+ * wobble correction.
61
+ * 'slitscan-both' — DEFAULT. V13.0a + no accept gate +
62
+ * feather blend. Iterate via per-stage
63
+ * toggles below.
64
+ *
65
+ * All three are A/B-comparable on the same scene by toggling here
66
+ * without restarting the app.
67
+ */
68
+ incrementalEngine: 'batch-keyframe' | 'hybrid' | 'slitscan-rotate' | 'slitscan-both';
69
+ /**
70
+ * V15 — Slit-scan slit width (fraction of pan-axis retained per
71
+ * frame). Range 0.10 – 0.70. Smaller = less within-slit multi-
72
+ * depth disagreement but tighter overlap budget at fast pans.
73
+ * Default 0.30. Only applied to slitscan-* engines.
74
+ */
75
+ slitWidthFraction: number;
76
+ /**
77
+ * V15 — Per-stage correction toggles for slitscan-both. Settings
78
+ * UI exposes these so iteration happens via toggles, not rebuilds.
79
+ */
80
+ acceptGate: 0 | 50;
81
+ enableTriangulation: boolean;
82
+ enableTriAccumulator: boolean;
83
+ enable2dNcc: boolean;
84
+ enableRansacHomography: boolean;
85
+ paintMode: 'FirstPaintedWins' | 'FeatherBlend';
86
+ hybridProjection: 'Cylindrical' | 'Planar';
87
+ /** 1D NCC search radius (slitscan-rotate only). */
88
+ nccSearchRadius1d: number;
89
+ /** **DEPRECATED in V15.0d** — see `planeSource`. Kept on the type
90
+ * for backward compat with stored settings. When `planeSource`
91
+ * is 'Disabled' (default) and this is true, the engine treats it
92
+ * as 'ARKitDetected'. */
93
+ useDetectedPlane: boolean;
94
+ /** V15.0d — source of the plane used by the V15.0b plane-projected
95
+ * stitch path. Slit-scan modes only.
96
+ *
97
+ * - 'Disabled': no plane projection (plain slit-scan).
98
+ * - 'ARKitDetected': use ARKit's first vertical plane that aligns
99
+ * with the camera's view direction. Falls back to slit-scan
100
+ * silently when no aligned plane is found.
101
+ * - 'Virtual': synthesize a plane perpendicular to the camera at
102
+ * `virtualPlaneDepthMeters` distance. Always works; loses
103
+ * "real depth" advantage but immune to ARKit picking the wrong
104
+ * surface (which is the common failure mode for ARKitDetected). */
105
+ planeSource: 'Disabled' | 'ARKitDetected' | 'Virtual';
106
+ /** V15.0d — depth (m) of the synthetic plane in front of the camera
107
+ * when `planeSource = 'Virtual'`. 0.3 – 5.0 m. Default 1.5 m. */
108
+ virtualPlaneDepthMeters: number;
109
+ /** V15.0d — alignment threshold (cosine) for ARKit-detected planes.
110
+ * Higher = stricter (fewer planes accepted). 0.0 – 1.0.
111
+ * Default 0.6 (≈53° max angle off-camera). */
112
+ arkitPlaneAlignmentThreshold: number;
113
+ /** V15.0g — plane-projection rendering style. Trapezoidal is the
114
+ * V15.0b legacy 3D-correct mapping; Rectified is V15.0g's clean-
115
+ * rectangle paste that eliminates tilt-induced trapezoidal
116
+ * distortion. Default Rectified. Ignored when planeSource =
117
+ * Disabled. */
118
+ planeProjectionStyle: 'Trapezoidal' | 'Rectified';
119
+ /** V15.0d — 2D NCC search half-window in pixels. 4 – 30.
120
+ * Default 12. */
121
+ nccSearchMargin2d: number;
122
+ /** V15.0d — 2D NCC confidence threshold below which corrections
123
+ * are rejected. 0.30 – 0.99. Default 0.75. */
124
+ nccConfidenceThreshold2d: number;
125
+ /** V15.0d (1B) — EMA smoothing on 2D NCC corrections to damp
126
+ * single-frame snaps. Default false. */
127
+ enableNcc2dEmaSmoothing: boolean;
128
+ /** V15.0d — EMA weight on the CURRENT-frame correction. 0.05 – 0.95.
129
+ * Default 0.4 (60% prev / 40% current). */
130
+ ncc2dEmaAlpha: number;
131
+ /** V15.0d (1C) — pan-axis-aware 2D NCC: clamp the cross-axis
132
+ * correction tighter than the pan-axis. Default false. */
133
+ enableNcc2dPanAxisLock: boolean;
134
+ /** V15.0d — cross-axis clamp (px) when pan-axis lock is on.
135
+ * 0 – 30. Default 5. */
136
+ ncc2dCrossAxisLockPx: number;
137
+ /** V16 — frame-selection mode for the live engine.
138
+ *
139
+ * - 'time-based' (default): every ARFrame is forwarded to the
140
+ * engine; the engine's own gate (kMinAcceptDeltaPx etc.) decides.
141
+ * Backward-compatible with all prior versions.
142
+ * - 'pose-based': frames are pre-filtered by a KeyframeGate that
143
+ * projects each onto the latched ARKit plane and accepts only
144
+ * when overlap with the previous keyframe is < 1 −
145
+ * overlapThreshold. Bounded to keyframeMaxCount frames per
146
+ * capture (matches iOS Camera / Samsung Pano architecture).
147
+ * Requires planeSource != 'Disabled' to engage.
148
+ * - 'flow-based' (V16 A2, DEFAULT): same KeyframeGate cap +
149
+ * threshold but the novelty metric is sparse-Lucas-Kanade
150
+ * optical flow on full-frame content instead of plane-projected
151
+ * polygon overlap. Plane-independent (scale-invariant — works
152
+ * regardless of latched plane size); the metric is "median
153
+ * pan-axis feature displacement / pan-axis frame dim", which is
154
+ * a direct measure of % new content on the leading edge. Falls
155
+ * back to angular delta when feature tracking fails (texture-
156
+ * poor scene / motion exceeds KLT pyramid window). */
157
+ frameSelectionMode: 'time-based' | 'pose-based' | 'flow-based';
158
+ /** V16 — required NEW-content fraction for a keyframe to be
159
+ * accepted (pose-based AND flow-based modes share this knob;
160
+ * both interpret 0.40 as "40 % new content"). Tuneable from
161
+ * 0.20 to 0.60 in the modal. */
162
+ keyframeOverlapThreshold: number;
163
+ /** V16 — hard cap on keyframes per capture (pose-based + flow-
164
+ * based modes). Default 6. Once reached, all further frames are
165
+ * rejected and the host should auto-finalize. */
166
+ keyframeMaxCount: number;
167
+ /** V16 A2 — flow-based mode: max Shi-Tomasi corners to detect per
168
+ * accepted keyframe. More = more robust median pan-axis
169
+ * displacement but slower detect (~15-25 ms at 150 on iPhone 13
170
+ * Pro). Range 50 – 300, default 150. */
171
+ flowMaxCorners: number;
172
+ /** V16 A2 — flow-based mode: Shi-Tomasi quality level (0, 1].
173
+ * Lower = more (weaker) corners detected; higher = fewer
174
+ * (stronger) corners. Default 0.01. Range 0.005 – 0.05 in the
175
+ * modal. */
176
+ flowQualityLevel: number;
177
+ /** V16 A2 — flow-based mode: minimum pixel distance between
178
+ * detected corners at WORKING resolution (the gate internally
179
+ * downscales the frame to 720 px longest side for KLT). Higher
180
+ * = more spatially-spread features. Default 10. */
181
+ flowMinDistance: number;
182
+ /** V16 — flow-based mode: translation budget in CENTIMETRES.
183
+ * When > 0, the gate force-accepts a frame if the camera has
184
+ * translated more than this distance (3D Euclidean) since the
185
+ * last accepted keyframe — even when novelty < threshold.
186
+ * Bounds the parallax between adjacent keyframes so the
187
+ * downstream affine stitcher matcher can fit a homography.
188
+ * Range 0 – 100 cm in the modal, default 0 = disabled.
189
+ * Recommended starting value once enabled: 8 cm. */
190
+ flowMaxTranslationCm: number;
191
+ /** V16 — flow-based mode: percentile used to aggregate tracked-
192
+ * feature absolute displacements into the novelty estimate.
193
+ * Pre-V16 used median (0.50); 0.85 picks up leading-edge
194
+ * motion sooner — matches user perception of "new content
195
+ * visible" better. Range 0.50 – 0.99, default 0.85. */
196
+ flowNoveltyPercentile: number;
197
+ /** V16 — flow-based mode: eval-throttle. Gate evaluation runs
198
+ * every Nth consumeFrame from the AR delegate instead of every
199
+ * frame. Pure CPU/battery savings — doesn't change WHICH
200
+ * frames are accepted, just samples less frequently. Range
201
+ * 1 – 10, default 1 (every frame). */
202
+ flowEvalEveryNFrames: number;
203
+ /** V15.0c — sliver position within the camera frame. 'Center' is
204
+ * V13.x default. 'Bottom' takes leading-edge content for top-to-
205
+ * bottom pan; 'Top' for bottom-to-top pan. */
206
+ sliverPosition: 'Center' | 'Bottom' | 'Top';
207
+ /** V15.0c — paint full first frame, then add slivers as user pans.
208
+ * Useful with 'Bottom' or 'Top' sliverPosition. */
209
+ firstFrameFullFrame: boolean;
210
+ /** Hard cap on hold duration (ms). 0 disables auto-stop. */
211
+ maxRecordingMs: number;
212
+ /** Frames per second of recording to sample for stitching. */
213
+ framesPerSecond: number;
214
+ /** Floor / ceiling on extracted frame count. */
215
+ minFrames: number;
216
+ maxFrames: number;
217
+ /** JPEG quality (0-100) for output panorama. */
218
+ quality: number;
219
+ /**
220
+ * 2026-05-14 (revised) — capture-source picker for the panorama
221
+ * camera screen. Two options after the 2026-05-14 user-reported
222
+ * Galaxy A35 crash + simplification request:
223
+ *
224
+ * 'ar' (DEFAULT) — Use the AR stack (ARKit on iOS, ARCore on
225
+ * Android). Plane detection, pose-aware
226
+ * capture, pose-driven gate. Falls back to
227
+ * non-AR silently if the device doesn't
228
+ * support AR.
229
+ * 'non-ar' — Use vision-camera. Disables all AR-based
230
+ * services (planeSource=Disabled, no plane
231
+ * polling, no AR session, frameSelectionMode
232
+ * flipped to flow-based). Lens-switcher chip
233
+ * on the capture screen lets the operator
234
+ * toggle 0.5× / 1× without re-opening Settings.
235
+ * The chip is hidden if the device has only
236
+ * one physical back lens.
237
+ *
238
+ * Cascade: switching from 'ar' → 'non-ar' triggers a useEffect
239
+ * in `AuditCaptureScreen` that patches dependent settings
240
+ * (planeSource, frameSelectionMode, useARPreview) to a coherent
241
+ * non-AR state. Operators don't have to know which other
242
+ * settings to flip.
243
+ *
244
+ * Earlier draft (replaced 2026-05-14) had 4 values:
245
+ * 'auto' | 'ar' | 'wide' | 'ultrawide'. The pre-mount
246
+ * physical-lens selection ('wide' / 'ultrawide') crashed the
247
+ * Galaxy A35 vision-camera CameraCaptureSession with a Parcel
248
+ * exception (physical_camera_id=null in AidlCamera3-Device
249
+ * configureStreams) — Camera2 can't be reliably steered to a
250
+ * specific physical lens via vision-camera's `physicalDevices`
251
+ * filter on this hardware. The post-mount on-screen chip path
252
+ * works because vision-camera selects the safe multi-lens
253
+ * virtual device first, and the lens swap happens against an
254
+ * already-open camera.
255
+ */
256
+ captureSource: 'ar' | 'non-ar';
257
+ /**
258
+ * 2026-05-16 (Issue 5) — diagnostic toast on every successful
259
+ * finalize. When `true`, the host renders a transient toast
260
+ * summarising the C+D progressive-confidence retry telemetry:
261
+ *
262
+ * "Stitch: 6/6 frames retained at thresh 1.00 (1 attempt)"
263
+ *
264
+ * Defaults to `false` so end-users don't see it. Toggle from the
265
+ * Settings modal under "Debug". Independent from any log-level
266
+ * controls — purely a UI affordance for field testing.
267
+ */
268
+ debug: boolean;
269
+ /**
270
+ * 2026-05-14 — `cv::Stitcher` pipeline mode for the batch stitch.
271
+ *
272
+ * 'auto' (DEFAULT)
273
+ * The capture engine looks at the accumulated translation vs
274
+ * rotation magnitudes between first and last accepted keyframe
275
+ * poses (AR-mode) or the windowed IMU integration (non-AR
276
+ * mode) and picks PANORAMA or SCANS at finalize time.
277
+ *
278
+ * 'panorama'
279
+ * `cv::Stitcher::PANORAMA` — rotation-only pipeline. Best for
280
+ * "rotate phone in place to capture a wide field of view"
281
+ * captures. ORB feature matching + global BundleAdjusterRay +
282
+ * SphericalWarper. Sharp seams, expensive memory. WARNING:
283
+ * on translation-heavy input the rotation-only homography fit
284
+ * diverges and the canvas can blow up to multi-GB on Android
285
+ * (2026-05-14 lmkd kill observed). Pick this only for genuine
286
+ * rotation panoramas.
287
+ *
288
+ * 'scans'
289
+ * `cv::Stitcher::SCANS` — translational pipeline. Best for
290
+ * "walk past a shelf and pan sideways" captures. Affine
291
+ * matcher + AffineBasedEstimator + BundleAdjusterAffine +
292
+ * PlaneWarper. Canvas size bounded by sum of frame areas.
293
+ * Slight quality drop on pure rotations but works for them too.
294
+ *
295
+ * iOS NOTE: as of 2026-05-14 the iOS stitcher uses a hand-rolled
296
+ * PANORAMA-style pipeline (OpenCVStitcher.mm:600+) regardless of
297
+ * this setting. Setting is passed through to iOS but ignored.
298
+ * Android honours it via image_stitcher_jni.cpp. Bridging iOS is
299
+ * a follow-up.
300
+ */
301
+ stitchMode: 'auto' | 'panorama' | 'scans';
302
+ }
303
+ export declare const DEFAULT_PANORAMA_SETTINGS: PanoramaSettings;
304
+ export interface PanoramaSettingsModalProps {
305
+ visible: boolean;
306
+ settings: PanoramaSettings;
307
+ onChange: (next: PanoramaSettings) => void;
308
+ onClose: () => void;
309
+ }
310
+ export declare function PanoramaSettingsModal({ visible, settings, onChange, onClose, }: PanoramaSettingsModalProps): React.JSX.Element;
311
+ //# sourceMappingURL=PanoramaSettingsModal.d.ts.map