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,930 @@
1
+ /**
2
+ * Incremental panorama-stitching native module bindings.
3
+ *
4
+ * See docs/site-content/design/2026-04-30-realtime-incremental-stitching.md
5
+ * for the architectural rationale. This file is the type-safe JS
6
+ * wrapper around the RN bridge; `useIncrementalStitcher` is the hook
7
+ * host code consumes; `IncrementalStitcherView` renders the live
8
+ * panorama preview.
9
+ */
10
+ import type { EmitterSubscription } from 'react-native';
11
+ /**
12
+ * Per-frame outcome returned by the engine. Mirrors the iOS
13
+ * `RLISFrameOutcome` enum and the Android equivalent — numeric
14
+ * values are kept identical so the JS layer doesn't branch on
15
+ * platform.
16
+ */
17
+ export declare enum IncrementalOutcome {
18
+ /** High-confidence accept — silent UX. */
19
+ AcceptedHigh = 0,
20
+ /** Accept with marginal confidence — show subtle yellow ring. */
21
+ AcceptedMedium = 1,
22
+ /** Frame too close to the previous accepted — wait for more pan. */
23
+ SkippedTooClose = 2,
24
+ /** Frame too far past the overlap window — show "slow down" hint. */
25
+ RejectedTooFar = 3,
26
+ /** Too few feature matches — show "scene too uniform" hint. */
27
+ RejectedSceneUniform = 4,
28
+ /** RANSAC failed or homography degenerate — show "alignment lost". */
29
+ RejectedAlignmentLost = 5,
30
+ /** AR tracking quality was poor — skip silently. */
31
+ SkippedTrackingPoor = 6,
32
+ /**
33
+ * V12.11 Step D — operator panned BACKWARDS past the running
34
+ * max along the pan axis. Engine has SKIPPED the paste; host
35
+ * should auto-finalize the capture (the most useful pano is
36
+ * what we have so far at the high-water mark). Emitted by
37
+ * the rectilinear engine only — cylindrical engines tolerate
38
+ * reverse motion via their warp pipeline.
39
+ */
40
+ RejectedReverseDirection = 7,
41
+ /**
42
+ * V16 — pose-driven keyframe gate rejected the frame because it
43
+ * overlapped >= (1 − overlapThreshold) with the last accepted
44
+ * keyframe. Host should keep showing the same status — user is
45
+ * mid-pan between two natural keyframe boundaries. No UX hint
46
+ * needed (this is the expected behaviour 90% of the time when
47
+ * pose-based selection is on).
48
+ */
49
+ SkippedKeyframeOverlap = 8,
50
+ /**
51
+ * V16 — pose-driven keyframe gate rejected the frame because the
52
+ * capture has hit `keyframeMaxCount` (default 6). Host should
53
+ * auto-finalize since no more frames will be accepted.
54
+ */
55
+ SkippedKeyframeMaxReached = 9
56
+ }
57
+ export interface IncrementalState {
58
+ /**
59
+ * Path to the latest panorama snapshot JPEG (file path, no
60
+ * `file://` prefix). Present only on accepted frames where a
61
+ * snapshot was written. Renders directly via RN's `<Image>`.
62
+ */
63
+ panoramaPath: string | null;
64
+ /** Width of the latest snapshot in pixels. 0 if no snapshot. */
65
+ width: number;
66
+ /** Height of the latest snapshot in pixels. 0 if no snapshot. */
67
+ height: number;
68
+ /** Total frames accepted into the panorama since `start()`. */
69
+ acceptedCount: number;
70
+ /** What happened to the most recent ARFrame the engine processed. */
71
+ outcome: IncrementalOutcome;
72
+ /** Composite confidence score [0, 1] for the most recent frame. */
73
+ confidence: number;
74
+ /**
75
+ * Estimated FoV-overlap with the previous accepted frame, in
76
+ * percent. Useful for pacing UX. -1 if first frame.
77
+ */
78
+ overlapPercent: number;
79
+ /** Wall-clock ms the most recent addPixelBuffer call took. */
80
+ processingMs: number;
81
+ /**
82
+ * V12.12 — engine-detected physical orientation, set from
83
+ * `R_panToCam` at first frame. TRUE for landscape capture
84
+ * (vertical pan), FALSE for portrait (horizontal pan). Stays
85
+ * at the FIRST-FRAME determination thereafter.
86
+ *
87
+ * **This is the single source of truth for orientation across
88
+ * the SDK + host.** JS-side hooks (e.g. `useDeviceOrientation`,
89
+ * `useWindowDimensions`) are unreliable when iOS interface-
90
+ * orientation lock is on; pose-derived detection is. UI
91
+ * components that need to know orientation (band overlay, dim
92
+ * bars, pan guide) MUST consume `state.isLandscape` rather
93
+ * than re-detecting.
94
+ *
95
+ * Defaults to `false` before the first frame is accepted (no
96
+ * pose to detect from yet). Hosts can fall back to a JS hook
97
+ * for the brief pre-capture preview if needed.
98
+ */
99
+ isLandscape: boolean;
100
+ /**
101
+ * V12.14.9 — running painted extent along the pan axis, in canvas
102
+ * pixels. Trailing edge of the most-recently-pasted slit. Pre-
103
+ * first-frame this is 0. After first-frame ≈ slit pan-axis size
104
+ * (~756 px for default kPanAxisFractionRect=0.7 on 1080-row sensor).
105
+ * Grows toward `panExtent` as the user pans.
106
+ *
107
+ * Used by the band overlay to compute `fillRatio = paintedExtent /
108
+ * panExtent`, which sizes the thumbnail proportional to pan
109
+ * progress. Replaces the V12.13 aspect-ratio-based formula that
110
+ * required the user to pan >1920 px before the thumb visibly grew.
111
+ *
112
+ * Defaults to 0 before the first frame.
113
+ */
114
+ paintedExtent: number;
115
+ /**
116
+ * V12.14.9 — total canvas pan-axis extent in pixels (engine config,
117
+ * default 5000). Constant for the lifetime of a capture. Used as
118
+ * the denominator for the fillRatio computation. Defaults to 0
119
+ * before the first frame.
120
+ */
121
+ panExtent: number;
122
+ /**
123
+ * V16 — pose-driven keyframe gate's max-keyframes cap for the
124
+ * current capture. When > 0, the JS status pill renders
125
+ * `Keyframes: acceptedCount / keyframeMax` so the operator can see
126
+ * the budget remaining. When 0, the keyframe gate is disabled
127
+ * (frameSelectionMode = "time-based") and the host should display
128
+ * acceptedCount as a raw counter without a denominator.
129
+ *
130
+ * Defaults to 0 before the first frame and stays 0 for the entire
131
+ * capture when the gate is disabled.
132
+ */
133
+ keyframeMax: number;
134
+ /**
135
+ * V16 Phase 1 — populated by the `batch-keyframe` engine on each
136
+ * keyframe-accepted event. Path to the JPEG saved under the
137
+ * session directory. Host can render a thumbnail from this path
138
+ * in the live-frame strip overlay so the operator sees what the gate accepted.
139
+ * Undefined for other engines and for non-accept events.
140
+ */
141
+ batchKeyframeThumbnailPath?: string;
142
+ /**
143
+ * V16 Phase 1 — zero-based keyframe index assigned by the
144
+ * collector when the JPEG was saved. Useful as a stable React key
145
+ * for the thumbnail strip.
146
+ */
147
+ batchKeyframeIndex?: number;
148
+ /**
149
+ * 2026-05-16 — realtime+batch fusion (Option A "Replace on
150
+ * completion"). True between the moment a hybrid-engine
151
+ * `finalize()` resolves with the live panorama AND the async
152
+ * refinement of the same keyframes through cv::Stitcher completes.
153
+ *
154
+ * During the refinement window the host should render a small
155
+ * "Refining…" pill so the operator knows a higher-quality result
156
+ * is on the way; the operator can continue browsing / starting
157
+ * another capture while the refinement runs.
158
+ *
159
+ * Stays false (or undefined) when the auto-trigger is a no-op —
160
+ * e.g. when the hybrid engine had nothing on disk to refine.
161
+ *
162
+ * See: docs/site-content/design/2026-05-14-realtime-batch-fusion.md
163
+ */
164
+ isRefining?: boolean;
165
+ /**
166
+ * 2026-05-16 — realtime+batch fusion (Option A). Path to the
167
+ * refined panorama JPEG written by `cv::Stitcher`. Emitted in a
168
+ * single state event when the async refinement completes (after
169
+ * the hybrid engine's `finalize()` has already returned the live
170
+ * `panoramaPath`).
171
+ *
172
+ * Host code should treat this as the canonical panorama for the
173
+ * remainder of the audit-capture flow when present, falling back
174
+ * to `panoramaPath` when absent. The refined output replaces the
175
+ * live output in-place — operator UX-wise it's the same JPEG slot,
176
+ * just sharper.
177
+ *
178
+ * Undefined when no refinement is in flight, when refinement fails,
179
+ * or when the auto-trigger was skipped because there were no
180
+ * keyframes on disk.
181
+ */
182
+ refinedPanoramaPath?: string;
183
+ }
184
+ export interface IncrementalStartOptions {
185
+ /**
186
+ * 2026-05-18 (Issue #2 regression fix) — frame source for the
187
+ * iOS engine.
188
+ *
189
+ * - 'arSession' (default) — engine registers as the
190
+ * ARSession's frame consumer. Use in AR captures. iOS
191
+ * bridge.start() requires `RNSARSession.start()` to
192
+ * have already been called.
193
+ *
194
+ * - 'jsDriver' — engine skips AR-session registration; JS
195
+ * feeds frames via `processFrameAtPath`. Use in iOS non-AR
196
+ * captures (vision-camera + gyro). No AR session required.
197
+ *
198
+ * Android ignores this option — its engine always accepts
199
+ * JS-driven frames.
200
+ */
201
+ frameSourceMode?: 'arSession' | 'jsDriver';
202
+ /** Compose-resolution width in pixels (default 720 for portrait, 960 for landscape). */
203
+ composeWidth?: number;
204
+ /** Compose-resolution height in pixels (default 960 for portrait, 720 for landscape). */
205
+ composeHeight?: number;
206
+ /** Pre-allocated canvas width (default 5000). */
207
+ canvasWidth?: number;
208
+ /** Pre-allocated canvas height (default 5000 — square so either
209
+ * pan axis fits without runtime grow logic). */
210
+ canvasHeight?: number;
211
+ /** Feather-blend band width in pixels (default 20). Unused after
212
+ * v5 hard-seam switch but kept for backwards compatibility. */
213
+ featherPx?: number;
214
+ /** JPEG quality for live snapshots [1, 100] (default 75). */
215
+ snapshotJpegQuality?: number;
216
+ /**
217
+ * Emit a snapshot on every Nth accepted frame (default 1 — every
218
+ * accept). Higher values save disk I/O at the cost of staler
219
+ * preview. Useful on lower-end Android.
220
+ */
221
+ snapshotEveryNAccepts?: number;
222
+ /**
223
+ * Per-frame rotation applied before any stitching work, in degrees.
224
+ * Must be one of `0`, `90`, `180`, `270`. Compute from the device's
225
+ * physical orientation:
226
+ * portrait → 90 (CW; panorama grows horizontally
227
+ * for the user's left↔right pan)
228
+ * portrait-upside-down → 270 (CCW)
229
+ * landscape-left → 0 (sensor already aligned)
230
+ * landscape-right → 0 (sensor already aligned)
231
+ *
232
+ * Default `90` because most shelf scans are done in portrait.
233
+ *
234
+ * @deprecated Use `captureOrientation` instead — it carries the
235
+ * landscape-left vs landscape-right distinction we need for
236
+ * correct output rotation per the two-modes spec
237
+ * (see memory/ar-stitching-two-modes.md). Once Phase 3 of the
238
+ * captureOrientation migration lands this field is removed.
239
+ */
240
+ frameRotationDegrees?: 0 | 90 | 180 | 270;
241
+ /**
242
+ * Physical phone orientation at capture start, classified by the
243
+ * accelerometer (`useDeviceOrientation`). Drives the output
244
+ * panorama's bake-rotation per the two supported capture modes:
245
+ *
246
+ * AR-STITCHING-TWO-MODES — see memory/ar-stitching-two-modes.md
247
+ *
248
+ * Mode A — landscape phone + vertical pan from top:
249
+ * 'landscape-left' → bake-rotate output 90° CCW
250
+ * 'landscape-right' → bake-rotate output 90° CW
251
+ * (mirror images of each other: world-up is on opposite
252
+ * sensor edges between L-left and L-right, so the
253
+ * rotations are opposite to land world-up at output-top)
254
+ *
255
+ * Mode B — portrait phone + horizontal pan from left:
256
+ * 'portrait' → no bake-rotation
257
+ * 'portrait-upside-down' → bake-rotate output 180°
258
+ *
259
+ * Any other combination of phone orientation + pan direction is a
260
+ * user deviation, not a supported mode. The engine still runs
261
+ * for unsupported combinations but the output rotation is a best-
262
+ * effort: the same mapping is applied.
263
+ *
264
+ * Defaults to `'portrait'` (Mode B start state) if not supplied.
265
+ */
266
+ captureOrientation?: 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
267
+ /**
268
+ * Engine mode (V15):
269
+ * 'hybrid' — Whole-frame projection + feature matching;
270
+ * planar projection by default (was cylindrical
271
+ * before V15; cylindrical can be re-enabled via
272
+ * `config.hybridProjection`).
273
+ * 'slitscan-rotate' — V13.0a baseline (pose-only paste, rectilinear,
274
+ * first-painted-wins) + 1D NCC for rotation
275
+ * wobble correction.
276
+ * 'slitscan-both' — DEFAULT. V13.0a baseline + no accept gate
277
+ * + feather blend. Iterate via `config`
278
+ * overrides (toggle triangulation / 2D NCC /
279
+ * RANSAC homography / paint mode etc.).
280
+ *
281
+ * Backward compat: 'firstwins-rectilinear' is mapped to
282
+ * 'slitscan-rotate'. Legacy 'firstwins', 'firstwins-zoomed', and
283
+ * 'slitscan' fall back to 'slitscan-both' with a deprecation warning
284
+ * in the native log.
285
+ */
286
+ engine?: 'hybrid' | 'slitscan-rotate' | 'slitscan-both' | 'batch-keyframe' | 'firstwins' | 'firstwins-zoomed' | 'firstwins-rectilinear' | 'slitscan';
287
+ /**
288
+ * V15 — per-stage correction config overrides. Mode-driven defaults
289
+ * are applied first (see RLISStitcherConfig +configForMode:); fields
290
+ * present here override those defaults. Any field may be omitted to
291
+ * accept the default.
292
+ */
293
+ config?: Partial<StitcherConfig>;
294
+ }
295
+ /**
296
+ * V15 — per-stage stitcher correction config. Each field is a runtime
297
+ * toggle/value; the native engine reads it on every ingest call. All
298
+ * fields optional (omit to accept the engine-mode default).
299
+ */
300
+ export interface StitcherConfig {
301
+ /** Fraction of pan-axis the rectilinear slit retains per frame.
302
+ * Range 0.10 – 0.70, default 0.30 in V15 slit-scan modes. */
303
+ kPanAxisFractionRect: number;
304
+ /** Minimum pan-axis advance (px) before a frame is accepted.
305
+ * 0 = accept on every consumeFrame (Apple-dense slit-scan, V15
306
+ * default). 50 = V13.0g default. */
307
+ kMinAcceptDeltaPx: number;
308
+ /** V13.0e+ ORB triangulation + median-Z parallax correction. */
309
+ enableTriangulation: boolean;
310
+ /** V13.0g per-accept incremental Δt accumulator on top of triangulation. */
311
+ enableTriAccumulator: boolean;
312
+ /** V15 1D NCC perpendicular-axis wobble correction (slitscan-rotate
313
+ * default). Independent of the other correction stages. */
314
+ enable1dNcc: boolean;
315
+ /** 1D NCC search radius in pixels (5 – 60). */
316
+ nccSearchRadius1d: number;
317
+ /** V13.0g 2D NCC fine-alignment after triangulation. */
318
+ enable2dNcc: boolean;
319
+ /** V14.0a RANSAC homography per slit + cv::warpPerspective. When
320
+ * enabled and successful, supersedes the rectangular paste path. */
321
+ enableRansacHomography: boolean;
322
+ /** 'FirstPaintedWins' protects already-painted pixels (V13.0e+
323
+ * default). 'FeatherBlend' alpha-blends new content into already-
324
+ * painted overlap pixels (V13.0d-style; V15 slitscan-both default). */
325
+ paintMode: 'FirstPaintedWins' | 'FeatherBlend';
326
+ /** 'Cylindrical' (V12.x – V14.0a behaviour) or 'Planar' (V15 default;
327
+ * cv::detail::PlaneWarper). Planar is well-behaved for pans <60°. */
328
+ hybridProjection: 'Cylindrical' | 'Planar';
329
+ /** V15.0c — where on the camera frame the per-accept sliver is taken.
330
+ * 'Center' (V13.x default), 'Bottom' (leading edge for top-to-bottom
331
+ * pan), or 'Top' (leading edge for bottom-to-top pan). */
332
+ sliverPosition: 'Center' | 'Bottom' | 'Top';
333
+ /** V15.0c — when true, the FIRST accepted frame paints the entire
334
+ * camera frame at canvas (0, 0); subsequent frames still use the
335
+ * configured sliver clip. Default false; set true when sliverPosition
336
+ * is Bottom/Top so the canvas is anchored with full-frame content. */
337
+ firstFrameFullFrame: boolean;
338
+ /** **DEPRECATED in V15.0d** — use `planeSource` instead.
339
+ *
340
+ * V15.0b boolean toggle for the plane-projected stitch path.
341
+ * Kept for backward compat: when `planeSource` is left at its
342
+ * default (Disabled), `useDetectedPlane = true` upgrades it to
343
+ * ARKitDetected. New callers should set `planeSource` directly. */
344
+ useDetectedPlane: boolean;
345
+ /** V15.0d — source of the plane used by the V15.0b plane-projected
346
+ * stitch path.
347
+ *
348
+ * - 'Disabled' (default): no plane projection; slit-scan path runs.
349
+ * - 'ARKitDetected': use ARKit's first vertical plane that aligns
350
+ * with the camera's view direction (filter threshold:
351
+ * `arkitPlaneAlignmentThreshold`). Falls back to slit-scan
352
+ * silently when no aligned plane is found.
353
+ * - 'Virtual': synthesize a plane at first frame: origin =
354
+ * camera_pos + `virtualPlaneDepthMeters` × camera_forward;
355
+ * normal = -camera_forward. Always works; no ARKit dependency.
356
+ *
357
+ * Field testing showed ARKit plane detection often picks the WRONG
358
+ * surface (side wall, doorframe) — Virtual mode is the safer
359
+ * default for arbitrary scenes. ARKitDetected wins when ARKit
360
+ * finds the correct fixture face. */
361
+ planeSource: 'Disabled' | 'ARKitDetected' | 'Virtual';
362
+ /** V15.0d — depth (metres) at which the synthetic plane is placed
363
+ * in front of the camera when `planeSource = Virtual`. Set to
364
+ * the user's typical scan distance. Range 0.3 – 5.0 m. Default
365
+ * 1.5 m. */
366
+ virtualPlaneDepthMeters: number;
367
+ /** V15.0d — minimum dot product between an ARKit-detected plane's
368
+ * surface normal and the camera's facing direction for the plane
369
+ * to be accepted (when `planeSource = ARKitDetected`). 1.0 =
370
+ * plane perfectly facing camera; 0.0 = plane edge-on; negative
371
+ * = facing away. Range 0.0 – 1.0. Default 0.6 (≈53° max angle
372
+ * off-camera). */
373
+ arkitPlaneAlignmentThreshold: number;
374
+ /** V15.0g — how the plane-projection helper renders each frame onto
375
+ * the canvas. Affects ARKitDetected and Virtual modes; ignored
376
+ * when planeSource = Disabled.
377
+ *
378
+ * - 'Trapezoidal' (V15.0b legacy): geometrically-correct 3D
379
+ * raycast. Each camera pixel maps to its plane intersection.
380
+ * Result is a trapezoid that grows distorted with tilt
381
+ * (cooler-bottom-2.3×-wider-than-top problem).
382
+ * - 'Rectified' (V15.0g default): camera frame pasted as a clean
383
+ * rectangle around its plane-projected anchor. Eliminates the
384
+ * tilt-induced trapezoidal distortion at the cost of strict 3D-
385
+ * correctness — the camera's per-pixel perspective stays inside
386
+ * the rectangle but doesn't reconcile across tilts. */
387
+ planeProjectionStyle: 'Trapezoidal' | 'Rectified';
388
+ /** V15.0d — 2D NCC search half-window in pixels. Was hardcoded
389
+ * ±12 in V15.0c.4. Smaller = less wandering on repetitive
390
+ * textures (peg holes, slatted panels), but easier to miss the
391
+ * true overlap when pose noise is high. Range 4 – 30. Default
392
+ * 12. */
393
+ nccSearchMargin2d: number;
394
+ /** V15.0d — 2D NCC confidence threshold below which the correction
395
+ * is rejected. Was hardcoded 0.75 in V15.0c.4. Higher = stricter,
396
+ * fewer false matches on repetitive textures, but more frames
397
+ * where NCC silently doesn't fire. Range 0.30 – 0.99. Default
398
+ * 0.75. */
399
+ nccConfidenceThreshold2d: number;
400
+ /** V15.0d (1B) — exponential-moving-average smoothing on 2D NCC
401
+ * corrections. When enabled, the applied correction is
402
+ * `α × current + (1−α) × prev` instead of just `current`. Damps
403
+ * single-frame snaps to spurious peaks. Default false. */
404
+ enableNcc2dEmaSmoothing: boolean;
405
+ /** V15.0d — EMA weight on the CURRENT-frame NCC correction
406
+ * (1 − α weight on the previous correction). Range 0.05 – 0.95.
407
+ * Default 0.4 (60% prev / 40% current — heavy damping). */
408
+ ncc2dEmaAlpha: number;
409
+ /** V15.0d (1C) — pan-axis-aware 2D NCC. When enabled, the cross-
410
+ * axis (perpendicular to pan) NCC correction is clamped tighter
411
+ * than the pan-axis (since 1D NCC + pose already handle cross-
412
+ * axis wobble). Default false. */
413
+ enableNcc2dPanAxisLock: boolean;
414
+ /** V15.0d — cross-axis clamp (pixels) for the pan-axis-aware mode.
415
+ * Range 0 – 30. Default 5. */
416
+ ncc2dCrossAxisLockPx: number;
417
+ /** V16 — how the engine decides which ARFrames to ingest.
418
+ *
419
+ * - 'time-based' (default): every frame the AR delegate delivers
420
+ * is forwarded to the engine; the engine's existing internal
421
+ * gate (kMinAcceptDeltaPx, time-throttled snapshot) decides
422
+ * accept/reject. Backward-compatible with all prior versions.
423
+ * - 'pose-based': frames are pre-filtered by a KeyframeGate. A
424
+ * frame is forwarded only when its projection onto the latched
425
+ * ARKit plane has at least `keyframeOverlapThreshold` of NEW
426
+ * area vs the last accepted keyframe. Bounded to
427
+ * `keyframeMaxCount` frames per capture. Mirrors how iOS
428
+ * Camera and Samsung Pano actually work. Requires
429
+ * `planeSource` != 'Disabled'; degrades silently to passthrough
430
+ * otherwise.
431
+ * - 'flow-based' (V16 A2): same KeyframeGate cap + threshold but
432
+ * the novelty metric is sparse-Lucas-Kanade optical flow on
433
+ * full-frame content rather than plane-projected polygon
434
+ * overlap. Plane-independent — no `planeSource` requirement;
435
+ * scale-invariant — works regardless of latched plane size.
436
+ * Falls back to angular delta when KLT tracking fails. */
437
+ frameSelectionMode: 'time-based' | 'pose-based' | 'flow-based';
438
+ /** V16 — required fraction of NEW content per keyframe (pose-based
439
+ * AND flow-based modes share this knob). Range 0.10 – 0.80.
440
+ * Default 0.40. Lower = more keyframes per capture + more
441
+ * redundancy + better feature matching but higher memory.
442
+ * Higher = fewer keyframes + less margin for blurry frames. */
443
+ keyframeOverlapThreshold: number;
444
+ /** V16 — hard cap on keyframes per capture (pose-based + flow-
445
+ * based modes). Range 3 – 10. Default 6 (matches Samsung's
446
+ * typical behaviour). Once reached, all subsequent frames are
447
+ * rejected and the host should auto-finalize. */
448
+ keyframeMaxCount: number;
449
+ /** V16 A2 — flow-based mode: max Shi-Tomasi corners detected per
450
+ * accepted keyframe. Range 50 – 300, default 150. Higher =
451
+ * more robust median pan-axis displacement; slower detect. */
452
+ flowMaxCorners: number;
453
+ /** V16 A2 — flow-based mode: Shi-Tomasi quality level (0, 1].
454
+ * Range 0.005 – 0.05, default 0.01. Lower = more (weaker)
455
+ * corners detected. */
456
+ flowQualityLevel: number;
457
+ /** V16 A2 — flow-based mode: minimum pixel distance between
458
+ * detected corners at WORKING resolution (gate downscales the
459
+ * frame to 720 px longest side internally). Range 5 – 20,
460
+ * default 10. */
461
+ flowMinDistance: number;
462
+ /** V16 — flow-based mode: translation budget in CENTIMETRES. When
463
+ * > 0, the gate force-accepts a frame if the camera has moved
464
+ * more than this distance (3D Euclidean) since the last accepted
465
+ * keyframe — even when novelty < keyframeOverlapThreshold.
466
+ * Bounds the parallax between adjacent keyframes so the
467
+ * downstream stitcher's matcher (AffineBestOf2NearestMatcher
468
+ * post-V16) sees inputs it can fit a homography to.
469
+ *
470
+ * Range 0 – 100 cm, default 0 = disabled. Recommended starting
471
+ * value once enabled: 8 cm. Set higher for fast pans, lower for
472
+ * precise multi-pass scans. */
473
+ flowMaxTranslationCm: number;
474
+ /** V16 — flow-based mode: percentile used to aggregate tracked-
475
+ * feature absolute displacements into the novelty estimate.
476
+ * Pre-V16 used median (0.50); 0.85 picks up the LEADING EDGE
477
+ * motion sooner — better matches user perception of "new
478
+ * content visible". Range 0.50 – 0.99, default 0.85. Set
479
+ * closer to 1.0 for more sensitive (catches even small leading-
480
+ * edge motion), closer to 0.5 for more conservative (needs
481
+ * half the features to have moved). */
482
+ flowNoveltyPercentile: number;
483
+ /** V16 — flow-based mode: eval-throttle. Gate evaluation runs
484
+ * every Nth consumeFrame from the AR delegate instead of every
485
+ * frame. Pure CPU/battery savings — doesn't change WHICH frames
486
+ * are accepted, just samples less frequently. Trade-off: up to
487
+ * N-1 frames of latency between "user moved enough" and "frame
488
+ * accepted". Range 1 – 10, default 1 (every frame).
489
+ *
490
+ * Recommended for long captures on devices that overheat: set 3
491
+ * for ~3× CPU reduction on the per-frame gate path. Eval cost
492
+ * is ~3-5 ms per call at 60 fps, so 3-5 ms / 16 ms ≈ 20-30 %
493
+ * AR-delegate budget freed when N=3. */
494
+ flowEvalEveryNFrames: number;
495
+ /** V16 Phase 1.fix3 — `cv::Stitcher`'s warper choice for the
496
+ * batch-keyframe finalize.
497
+ *
498
+ * - 'plane': flat output, best when camera angles stay near
499
+ * perpendicular to scene. Unbounded bbox for tilt-heavy pans
500
+ * (umatrix.cpp:710 crash).
501
+ * - 'cylindrical': wraps onto a cylinder with FIXED vertical axis.
502
+ * Good for horizontal pans; unrolls vertical pans along the wrong
503
+ * axis (output looks rotated 90°).
504
+ * - 'spherical' (recommended for batch-keyframe): rotationally
505
+ * symmetric, handles any pan direction. Mild uniform curvature.
506
+ *
507
+ * Native default is "spherical" specifically for batch-keyframe
508
+ * (overrides this prop's value in `IncrementalStitcher.start`
509
+ * unless explicitly provided). Same field is also consumed by the
510
+ * legacy non-AR batch path (`BatchStitcher.stitchVideo`) where
511
+ * the historical default is "plane". */
512
+ warperType: 'plane' | 'cylindrical' | 'spherical';
513
+ /** V16 Phase 1.fix3 — `cv::Stitcher`'s blender choice for the
514
+ * batch-keyframe finalize.
515
+ * - 'multiband' (default): Laplacian-pyramid blending; best seam
516
+ * quality, higher peak memory.
517
+ * - 'feather': single-band alpha; faster, no halo artifacts when
518
+ * exposure varies. */
519
+ blenderType: 'multiband' | 'feather';
520
+ /** V16 Phase 1.fix3 — `cv::Stitcher`'s seam-finder choice.
521
+ * - 'graphcut' (default): cv::detail::GraphCutSeamFinder; optimal
522
+ * seams, pairs with multi-band, holds all warped frames in memory.
523
+ * - 'skip': stream warp+feed, lower peak memory, fine with feather. */
524
+ seamFinderType: 'graphcut' | 'skip';
525
+ /** V16 Phase 1b.fix5c — toggle the max-inscribed-rectangle crop in
526
+ * the batch-keyframe finalize pipeline. When false (default), the
527
+ * output is cropped to `cv::boundingRect(mask)` only — preserves
528
+ * all stitched content at the cost of possible black corners
529
+ * where cv::Stitcher's projection didn't fill. When true, the
530
+ * pipeline additionally runs `MaxInscribedRectFromMask` +
531
+ * morphological-close + column-projection second pass for a
532
+ * clean-cornered rectangle (but can over-aggressively shrink the
533
+ * output on lopsided masks). Surfaced as a settings toggle so
534
+ * the operator can A/B the two crop strategies on real scenes. */
535
+ enableMaxInscribedRectCrop: boolean;
536
+ /** 2026-05-14 — `cv::Stitcher` pipeline mode for the batch-keyframe
537
+ * finalize step.
538
+ *
539
+ * 'auto' (default) — Engine picks PANORAMA or SCANS at finalize
540
+ * time based on accumulated translation vs
541
+ * rotation magnitudes between first and last
542
+ * accepted keyframe poses (AR mode) or the
543
+ * windowed IMU integration (non-AR mode).
544
+ * 'panorama' — Force cv::Stitcher::PANORAMA (rotation-only
545
+ * pipeline; ORB + HomographyBasedEstimator +
546
+ * BundleAdjusterRay + SphericalWarper).
547
+ * Best for rotate-in-place panoramas.
548
+ * WARNING: on translation-heavy input the
549
+ * rotation-only model diverges and the
550
+ * compositing canvas can grow to multi-GB
551
+ * (Android lmkd kill observed 2026-05-14).
552
+ * 'scans' — Force cv::Stitcher::SCANS (affine pipeline;
553
+ * AffineBestOf2NearestMatcher +
554
+ * BundleAdjusterAffine + PlaneWarper).
555
+ * Best for walk-past-shelf captures. Canvas
556
+ * size bounded by sum of frame areas.
557
+ *
558
+ * iOS note: as of 2026-05-14 iOS uses a hand-rolled PANORAMA-style
559
+ * pipeline regardless of this setting. Setting is passed through
560
+ * to iOS but currently ignored; Android honours it via
561
+ * `image_stitcher_jni.cpp` + `IncrementalStitcher.kt`. */
562
+ stitchMode: 'auto' | 'panorama' | 'scans';
563
+ /** 2026-05-14 (revised) — capture source axis.
564
+ *
565
+ * 'ar' — ARKit / ARCore session feeds the engine.
566
+ * 'non-ar' — vision-camera feeds the engine via the gyro-driven
567
+ * Android snapshot loop (or iOS equivalent — see
568
+ * realtime-batch-fusion design doc Out-of-Scope).
569
+ * Lens choice (0.5× / 1×) is handled by the on-screen
570
+ * chip after mount, not by this setting.
571
+ *
572
+ * Native side uses this to:
573
+ * 1. Decide whether the KeyframeGate should DISABLE its angular-
574
+ * delta fallback path. Non-AR has no usable pose data → the
575
+ * angular calc would produce nonsense → set `disableAngularFallback`
576
+ * true on the gate.
577
+ * 2. Decide whether to expect pose updates through the AR delegate
578
+ * path (only meaningful when source='ar').
579
+ *
580
+ * Earlier draft (replaced 2026-05-14) had 4 values:
581
+ * 'ar' | 'wide' | 'ultrawide' | 'auto'. Pre-mount physical-lens
582
+ * selection via vision-camera's `physicalDevices` filter crashed
583
+ * Galaxy A35's CameraCaptureSession with a Parcel exception
584
+ * (physical_camera_id=null in AidlCamera3-Device configureStreams).
585
+ * Switched to post-mount chip-driven lens swap. */
586
+ captureSource: 'ar' | 'non-ar';
587
+ }
588
+ export interface IncrementalFinalizeResult {
589
+ /** Path to the final panorama JPEG written to `outputPath`. */
590
+ panoramaPath: string;
591
+ width: number;
592
+ height: number;
593
+ acceptedCount: number;
594
+ /** Frames the engine queue dropped due to backpressure (diagnostic). */
595
+ droppedBackpressure: number;
596
+ /** 2026-05-15 (D) — batch-keyframe stitcher telemetry. Populated
597
+ * by the cv::Stitcher PANORAMA / SCANS path. Surfaces
598
+ * `leaveBiggestComponent` drops so the host UI can warn the
599
+ * operator when boundary frames were excluded due to weak feature-
600
+ * matching confidence.
601
+ *
602
+ * Undefined on the realtime (hybrid / firstwins) engines — those
603
+ * don't run leaveBiggestComponent.
604
+ *
605
+ * framesRequested: number of keyframes handed to the
606
+ * stitcher (== acceptedCount for batch).
607
+ * framesIncluded: number of keyframes retained after
608
+ * leaveBiggestComponent pruning.
609
+ * framesDropped: framesRequested − framesIncluded.
610
+ * > 0 means the stitcher silently
611
+ * dropped boundary frames; surface a
612
+ * "Stitched N of M frames" toast.
613
+ * finalConfidenceThresh: panoConfidenceThresh value used on
614
+ * the successful attempt (1.0 / 0.5 /
615
+ * 0.3 — see image_stitcher_jni.cpp
616
+ * retry loop). Useful for debugging
617
+ * scenes that consistently need a
618
+ * lower threshold. */
619
+ framesRequested?: number;
620
+ framesIncluded?: number;
621
+ framesDropped?: number;
622
+ finalConfidenceThresh?: number;
623
+ }
624
+ /**
625
+ * 2026-05-16 — input to `refinePanorama`. Mirrors the subset of
626
+ * `StitcherConfig` that affects the batch refinement step
627
+ * (`cv::Stitcher` pipeline knobs). All fields optional — when
628
+ * omitted the native side picks production-tested defaults that
629
+ * match the existing batch-keyframe finalize path:
630
+ *
631
+ * warperType = "spherical" (handles any pan direction)
632
+ * blenderType = "multiband"
633
+ * seamFinderType = "graphcut"
634
+ * captureOrientation = "portrait"
635
+ * useInscribedRectCrop = false
636
+ * stitchMode = "auto" (Android only; iOS hand-rolled
637
+ * pipeline is PANORAMA regardless).
638
+ * NOTE: on the explicit `refinePanorama`
639
+ * path, Android collapses "auto" to
640
+ * "scans" — affine, not rotational —
641
+ * because refinement is the slow-path
642
+ * quality bake where SCANS' translation
643
+ * tolerance pays off. The "auto" name is
644
+ * kept for API symmetry with the live
645
+ * pipeline, but it is NOT cv::Stitcher's
646
+ * PANORAMA mode on this path.
647
+ * jpegQuality = 90
648
+ *
649
+ * Resolution budgets (`*ResolMP`) keep cv::Stitcher's staged-pipeline
650
+ * memory bounded — see image_stitcher_jni.cpp on Android and the
651
+ * shared C++ `StitchConfig` for the full rationale. Passing a
652
+ * negative value or omitting the field keeps the per-platform safe
653
+ * default (Android compose-MP cap of 1.0, iOS manual-pipeline cap of
654
+ * 0.6).
655
+ *
656
+ * See: docs/site-content/design/2026-05-14-realtime-batch-fusion.md
657
+ */
658
+ export interface IncrementalRefineOptions {
659
+ /** "plane" | "cylindrical" | "spherical". Default "spherical". */
660
+ warperType?: 'plane' | 'cylindrical' | 'spherical';
661
+ /** "multiband" | "feather". Default "multiband". */
662
+ blenderType?: 'multiband' | 'feather';
663
+ /** "graphcut" | "skip". Default "graphcut". */
664
+ seamFinderType?: 'graphcut' | 'skip';
665
+ /** Drives the OUTPUT bake-rotation. Default "portrait". */
666
+ captureOrientation?: 'portrait' | 'portrait-upside-down' | 'landscape-left' | 'landscape-right';
667
+ /** Crop to max-inscribed rectangle. Default false (bbox crop only). */
668
+ useInscribedRectCrop?: boolean;
669
+ /**
670
+ * Android: `cv::Stitcher` pipeline mode. Default "auto".
671
+ *
672
+ * On the explicit `refinePanorama` path, "auto" silently collapses
673
+ * to "scans" (affine). This is intentional: refinement is the
674
+ * slow-path quality bake where SCANS' translation tolerance gives
675
+ * a noticeably better stitch than PANORAMA's rotation-only model.
676
+ * Pass "panorama" explicitly if you need rotational behaviour.
677
+ *
678
+ * iOS ignores this field — the hand-rolled `cv::detail::*` pipeline
679
+ * in `cpp/stitcher.cpp` is functionally equivalent to PANORAMA
680
+ * regardless of what you pass here.
681
+ */
682
+ stitchMode?: 'auto' | 'panorama' | 'scans';
683
+ /** JPEG quality 1..100, default 90. */
684
+ jpegQuality?: number;
685
+ }
686
+ /**
687
+ * 2026-05-16 — result of an explicit `refinePanorama` call. Mirrors
688
+ * `IncrementalFinalizeResult` so host code can treat refined results
689
+ * the same way it treats batch-keyframe finalize results.
690
+ */
691
+ export interface IncrementalRefineResult {
692
+ /** Path to the refined panorama JPEG written to `outputPath`. */
693
+ panoramaPath: string;
694
+ width: number;
695
+ height: number;
696
+ /** Frames the stitcher saw (== framePaths.length). */
697
+ framesRequested: number;
698
+ /** Frames retained after `leaveBiggestComponent` (≤ framesRequested). */
699
+ framesIncluded: number;
700
+ /** framesRequested − framesIncluded. > 0 = some frames dropped. */
701
+ framesDropped: number;
702
+ /** The confidence threshold that succeeded. -1 when not applicable. */
703
+ finalConfidenceThresh: number;
704
+ }
705
+ /**
706
+ * V15.0e — ARKit plane detection state, polled by the capture screen
707
+ * UI when planeSource=ARKitDetected. Used to render a status pill:
708
+ *
709
+ * - status === 'searching': no candidate plane seen yet. UI shows
710
+ * a red/amber "Looking for wall plane…" pill and a hint to aim
711
+ * at a textured area for a few seconds.
712
+ * - status === 'evaluating': ARKit found candidate plane(s) but
713
+ * the alignment filter rejected them all. UI shows the
714
+ * bestAlignment so the operator can see they're CLOSE
715
+ * ("plane found but off-axis (best 0.45)") and aim more
716
+ * directly at the wall.
717
+ * - status === 'ready': plane is latched. UI shows green "Plane
718
+ * locked" and enables the Capture (hold-to-scan) button.
719
+ */
720
+ export interface ARPlaneStatus {
721
+ status: 'searching' | 'evaluating' | 'ready';
722
+ hasPlane: boolean;
723
+ /** Best rejected-alignment score seen so far. -1 = no candidate yet.
724
+ * Range [-1, 1]; positive when at least one candidate was evaluated. */
725
+ bestAlignment: number;
726
+ /** Current alignment threshold (matches the engine config). */
727
+ threshold: number;
728
+ }
729
+ interface NativeIncrementalModule {
730
+ start(options: IncrementalStartOptions): Promise<{
731
+ ok: true;
732
+ }>;
733
+ /**
734
+ * Finalize the running capture and write the final panorama JPEG.
735
+ *
736
+ * `outputPath` (optional) — when empty/omitted the native side
737
+ * creates a path under the app's tmp directory and returns it
738
+ * inside the `panoramaPath` field of the result. Host apps that
739
+ * want the stitched panorama to be USER-VISIBLE (e.g., browsable
740
+ * via iOS Files.app) should pass a path under the app's
741
+ * Documents directory, e.g.
742
+ * `${RNFS.DocumentDirectoryPath}/captures/${auditId}.jpg`
743
+ * (or the platform-equivalent on Android). Two host-side
744
+ * requirements for Files.app exposure on iOS:
745
+ *
746
+ * 1. Info.plist must set `UIFileSharingEnabled = true` so the
747
+ * app's Documents directory is exposed via the Files
748
+ * browser at all.
749
+ * 2. Info.plist must set `LSSupportsOpeningDocumentsInPlace
750
+ * = true` so users can open the files in-place rather than
751
+ * requiring a copy.
752
+ *
753
+ * Frames (intermediate keyframe JPEGs) are saved by the engine
754
+ * to its own private directory and are NOT auto-cleaned — see
755
+ * `cleanupKeyframes` for the GC hook host apps should call on
756
+ * launch or via a lifecycle event.
757
+ */
758
+ finalize(options: {
759
+ outputPath?: string;
760
+ quality?: number;
761
+ /**
762
+ * 2026-05-18 (iOS cross-orientation fix) — JS-supplied current
763
+ * device orientation at finalize time. When provided, the
764
+ * engine uses this for the bake-rotation pass in place of the
765
+ * orientation captured at start(). Closes the cross-orientation
766
+ * hole where the user starts in one orientation and pans/
767
+ * captures in another — the start-time snapshot would otherwise
768
+ * bake to the wrong direction. Valid values match
769
+ * IncrementalStartOptions.captureOrientation; omit/empty to keep
770
+ * legacy start-time behaviour.
771
+ */
772
+ captureOrientation?: string;
773
+ }): Promise<IncrementalFinalizeResult>;
774
+ cancel(): Promise<{
775
+ ok: true;
776
+ }>;
777
+ getState(): Promise<IncrementalState | null>;
778
+ /** V15.0e — poll AR plane detection state. Polled at ~2 Hz when
779
+ * planeSource=ARKitDetected so the status pill updates live. */
780
+ getARPlaneStatus(): Promise<ARPlaneStatus>;
781
+ /** V15.0g — clear the latched ARKit plane and re-evaluate all
782
+ * currently-tracked vertical planes against the camera's CURRENT
783
+ * aim, picking the largest plane that passes the alignment
784
+ * threshold. Called by the capture screen on hold-to-scan press
785
+ * so the latched plane reflects what the operator is aiming at
786
+ * right NOW, not whichever plane ARKit noticed first. */
787
+ relatchARPlane(): Promise<{
788
+ latched: boolean;
789
+ }>;
790
+ /** V16 — arm the pose-driven keyframe gate to force-accept the
791
+ * next ARFrame regardless of overlap. Called by the capture
792
+ * screen on shutter release so the trailing edge of the scan
793
+ * isn't truncated when the user releases mid-pan. No-op when
794
+ * the gate is disabled (frameSelectionMode = 'time-based'). */
795
+ markNextFrameAsLastKeyframe(): Promise<{
796
+ ok: true;
797
+ }>;
798
+ /** V16 Phase 1b.fix2 — poll the process phys_footprint in MB.
799
+ * Backs the on-screen memory debug overlay. Same metric iOS
800
+ * jetsam evaluates against, so the displayed value is the
801
+ * one-true-number for "how close are we to OOM?". Returns -1
802
+ * on task_info failure (very rare). Resolves immediately. */
803
+ getMemoryFootprintMB(): Promise<number>;
804
+ /**
805
+ * 2026-05-16 — realtime+batch fusion API foundation. Run the
806
+ * shared C++ `cv::Stitcher` pipeline over a caller-supplied list
807
+ * of keyframe JPEG paths and write a refined panorama to
808
+ * `outputPath`.
809
+ *
810
+ * Pre-conditions:
811
+ * - `framePaths.length >= 2`
812
+ * - Each path must exist on disk (the native side will read it
813
+ * via cv::imread); rejected otherwise.
814
+ *
815
+ * Per-platform routing:
816
+ * - iOS: `OpenCVStitcher.stitchFramePaths(...)`
817
+ * (manual cv::detail::* pipeline, useManualPipeline=true).
818
+ * - Android: `BatchStitcher.stitchSync(...)` →
819
+ * `image_stitcher_jni.cpp` (high-level
820
+ * cv::Stitcher::create() pipeline).
821
+ *
822
+ * Reuses the same C++ stitcher both platforms use for the
823
+ * batch-keyframe `finalize()` path — so refinement quality on
824
+ * arbitrary keyframe sets matches what the batch-keyframe engine
825
+ * has been producing in production.
826
+ *
827
+ * The auto-trigger inside the hybrid engine's `finalize()` is a
828
+ * separate code path that internally calls `refinePanorama` when
829
+ * keyframes are on disk; host code may also call it explicitly to
830
+ * re-refine after editing the keyframe set.
831
+ */
832
+ refinePanorama(options: {
833
+ framePaths: string[];
834
+ outputPath: string;
835
+ config?: IncrementalRefineOptions;
836
+ }): Promise<IncrementalRefineResult>;
837
+ /** PiP investigation only — write a JS-side message into the
838
+ * Swift-side rlis-debug.log so we get a single timeline. */
839
+ appendDebugLog?(message: string): Promise<{
840
+ ok: true;
841
+ }>;
842
+ /**
843
+ * 2026-05-18 (Iss 3) — delete keyframe JPEGs older than the cutoff
844
+ * from the SDK's intermediate-keyframe storage directory.
845
+ *
846
+ * Background: the batch-keyframe capture mode saves accepted
847
+ * frames as JPEGs in a per-session directory (iOS:
848
+ * `Library/Application Support/Captures/{uuid}/`, Android: app's
849
+ * private files dir under `captures/{uuid}/`). These are kept
850
+ * across runs so post-hoc re-stitching is possible from the
851
+ * debug menu — but they accumulate over time and bloat user
852
+ * storage. Host apps should call this on launch or on a
853
+ * lifecycle hook to garbage-collect old sessions.
854
+ *
855
+ * `olderThanMs` is the staleness cutoff in milliseconds. Sessions
856
+ * whose newest file mtime is older than `Date.now() - olderThanMs`
857
+ * are deleted in full. Default if omitted: 24 hours. Pass 0 to
858
+ * delete every keyframe session unconditionally (use with care).
859
+ *
860
+ * Resolves with the count of deleted sessions + total bytes freed,
861
+ * so the host can surface a "freed 42 MB of old captures"
862
+ * confirmation if desired. Rejects on filesystem errors (e.g.,
863
+ * the captures dir does not exist — which is also fine; pass 0
864
+ * sessions back) — implementations should swallow ENOENT-style
865
+ * errors and resolve with zero counts.
866
+ */
867
+ cleanupKeyframes?(options?: {
868
+ olderThanMs?: number;
869
+ }): Promise<{
870
+ sessionsDeleted: number;
871
+ bytesFreed: number;
872
+ }>;
873
+ /**
874
+ * 2026-05-18 (Iss 3) — return the absolute filesystem path of the
875
+ * directory where keyframe JPEGs for the CURRENT (running)
876
+ * capture are being saved. Returns an empty string when no
877
+ * capture is in flight or when the engine isn't using a per-
878
+ * session keyframe directory (e.g., hybrid mode without the
879
+ * batch-keyframe collector).
880
+ *
881
+ * Mainly useful for debugging — e.g., the host can dump the
882
+ * directory's contents to the on-screen log, or copy it to
883
+ * /Documents for post-hoc inspection.
884
+ */
885
+ getKeyframeDir?(): Promise<{
886
+ path: string;
887
+ }>;
888
+ }
889
+ /**
890
+ * Lazy-resolve the native module. Returns null on platforms that
891
+ * don't have it registered yet (e.g. older builds without the new
892
+ * native code). Callers fall back to the batch stitcher in that
893
+ * case.
894
+ */
895
+ export declare function getIncrementalNativeModule(): NativeIncrementalModule | null;
896
+ /**
897
+ * Whether the native incremental stitcher is registered and ready.
898
+ * Equivalent to `getIncrementalNativeModule() !== null`; provided
899
+ * as a convenience export so host code reads cleanly.
900
+ */
901
+ export declare function incrementalStitcherIsAvailable(): boolean;
902
+ /**
903
+ * 2026-05-18 (Iss 3) — host-callable helper to clean up old
904
+ * keyframe sessions. Wraps the native `cleanupKeyframes` with a
905
+ * sensible default (24 hours) and a noop fallback when the native
906
+ * method isn't implemented (older SDK builds).
907
+ *
908
+ * Typical use: call this in App.tsx's mount effect or from a
909
+ * background-task hook so storage stays bounded between captures.
910
+ *
911
+ * Resolves with the count of sessions deleted + bytes freed so the
912
+ * host can log / surface a "cleaned up X MB" message. Never
913
+ * rejects — filesystem failures (including ENOENT on the captures
914
+ * dir) resolve as `{ sessionsDeleted: 0, bytesFreed: 0 }`.
915
+ */
916
+ export declare function cleanupOldKeyframes(options?: {
917
+ olderThanMs?: number;
918
+ }): Promise<{
919
+ sessionsDeleted: number;
920
+ bytesFreed: number;
921
+ }>;
922
+ /**
923
+ * Subscribe to per-frame state updates emitted by the native engine.
924
+ * The returned `EmitterSubscription` MUST be removed when no longer
925
+ * needed (`subscription.remove()`); leaks here cause memory growth
926
+ * across captures.
927
+ */
928
+ export declare function subscribeIncrementalState(listener: (state: IncrementalState) => void): EmitterSubscription | null;
929
+ export {};
930
+ //# sourceMappingURL=incremental.d.ts.map