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,473 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // OpenCVIncrementalStitcher.h
4
+ //
5
+ // Per-frame incremental panorama stitcher. Replaces the batch-mode
6
+ // `cv::Stitcher` flow used by `OpenCVStitcher` with a streaming
7
+ // pipeline: each accepted frame is matched against the previous,
8
+ // warped via a RANSAC homography, and feather-blended onto a running
9
+ // canvas — no end-of-capture wait.
10
+ //
11
+ // Why incremental:
12
+ // See docs/site-content/design/2026-04-30-realtime-incremental-stitching.md
13
+ // for the full motivation. TL;DR: live preview, bounded memory,
14
+ // fail-fast on bad frames, no terminal BA stall.
15
+ //
16
+ // What this file owns:
17
+ // The C++/OpenCV side of the engine — feature extraction, matching,
18
+ // RANSAC, warp, feather blend. All `cv::*` types stay inside the
19
+ // `.mm` impl; this header exposes only Foundation types so it can
20
+ // be imported from pure Swift via the umbrella header.
21
+ //
22
+ // Threading:
23
+ // Methods on this class are NOT thread-safe internally. The Swift
24
+ // layer (`IncrementalStitcher`) owns a serial queue and
25
+ // funnels all calls through it. The lock is intentional: live
26
+ // capture wants ordered frame ingestion, not parallel mutation of
27
+ // the canvas.
28
+ //
29
+
30
+ #import <Foundation/Foundation.h>
31
+ #import <CoreVideo/CoreVideo.h>
32
+
33
+ NS_ASSUME_NONNULL_BEGIN
34
+
35
+ /// NSError domain raised by incremental stitcher errors.
36
+ extern NSString *const RNImageStitcherIncrementalErrorDomain;
37
+
38
+ /// Per-frame outcome — drives the JS-side UX (silent accept, subtle
39
+ /// flag, explicit hint).
40
+ typedef NS_ENUM(NSInteger, RLISFrameOutcome) {
41
+ /// Frame accepted with high confidence. Silent UX update.
42
+ RLISFrameOutcomeAcceptedHigh = 0,
43
+ /// Frame accepted but match quality was middling. Show subtle
44
+ /// confidence flag (yellow ring) — not an error, just informational.
45
+ RLISFrameOutcomeAcceptedMedium = 1,
46
+ /// Frame skipped because pose hasn't moved enough since last accept.
47
+ /// Normal — operator hasn't panned past the overlap window yet.
48
+ RLISFrameOutcomeSkippedTooClose = 2,
49
+ /// Frame skipped because pose moved too far since last accept —
50
+ /// operator panned past the overlap window before another accept.
51
+ /// JS shows a "slow down" hint.
52
+ RLISFrameOutcomeRejectedTooFar = 3,
53
+ /// Feature matching produced too few correspondences. Scene is
54
+ /// likely uniform/textureless or the frame is motion-blurred.
55
+ /// JS shows a "scene too uniform" hint.
56
+ RLISFrameOutcomeRejectedSceneUniform = 4,
57
+ /// RANSAC homography failed or produced a degenerate transform.
58
+ /// JS shows an "alignment lost — slow down" hint.
59
+ RLISFrameOutcomeRejectedAlignmentLost = 5,
60
+ /// Tracking state from the AR session was poor at the time of
61
+ /// this frame — no point trying to incorporate it.
62
+ RLISFrameOutcomeSkippedTrackingPoor = 6,
63
+ /// V12.11 Step D — operator has panned BACKWARDS past the
64
+ /// running max along the pan axis by more than
65
+ /// `kReverseStopPx`. Engine has SKIPPED the paste; host should
66
+ /// auto-finalize the capture and surface the panorama as it
67
+ /// stood at the running-max position. Emitted by the
68
+ /// rectilinear engine only — cylindrical engines tolerate
69
+ /// reverse motion via their warp pipeline.
70
+ RLISFrameOutcomeRejectedReverseDirection = 7,
71
+ };
72
+
73
+ /// Telemetry returned alongside each addFrame call — host can log
74
+ /// these to refine threshold tuning during field testing.
75
+ @interface RLISFrameTelemetry : NSObject
76
+ @property (nonatomic, readonly) RLISFrameOutcome outcome;
77
+ /// Estimated FoV-overlap with the previously accepted frame, in
78
+ /// percent. Computed from pose-delta + intrinsics, NOT from
79
+ /// matched features (which would require running the matcher
80
+ /// every frame). Range [0, 100]. -1 if first frame.
81
+ @property (nonatomic, readonly) double overlapPercent;
82
+ /// Number of feature matches that survived ratio-test filtering.
83
+ /// Zero unless the frame went through feature matching (i.e.
84
+ /// passed the pose-delta gate).
85
+ @property (nonatomic, readonly) NSInteger matchCount;
86
+ /// Fraction of matches that survived RANSAC inlier filtering.
87
+ /// Range [0, 1]. Zero unless the frame went through RANSAC.
88
+ @property (nonatomic, readonly) double inlierRatio;
89
+ /// Composite confidence score [0, 1].
90
+ @property (nonatomic, readonly) double confidence;
91
+ /// Wall-clock milliseconds the addFrame call took (end-to-end).
92
+ @property (nonatomic, readonly) double processingMs;
93
+ /// V12.12 — physical device orientation as detected by the engine
94
+ /// from `R_panToCam` at first frame. TRUE for landscape capture
95
+ /// (vertical pan), FALSE for portrait capture (horizontal pan).
96
+ /// Stays at the FIRST-FRAME determination for the rest of the
97
+ /// capture (orientation can't physically change without restarting
98
+ /// pano). Defaults to FALSE (portrait) before first frame.
99
+ ///
100
+ /// JS side reads this from `IncrementalState.isLandscape` to drive
101
+ /// orientation-aware UI (band overlay, dim bars). This is the
102
+ /// single source of truth for orientation across the SDK + host —
103
+ /// the V12.6 fix established that JS-side orientation hooks are
104
+ /// unreliable under iOS interface-orientation lock; pose detection
105
+ /// is.
106
+ @property (nonatomic, readonly) BOOL isLandscape;
107
+
108
+ /// V12.14.9 — running max paint position along the pan axis, in
109
+ /// canvas pixels. In landscape mode (`isLandscape == TRUE`) this
110
+ /// is the canvas Y at which the most-recently-pasted slit ends;
111
+ /// in portrait mode (`isLandscape == FALSE` = portrait+horizontal-pan
112
+ /// per the two-mode spec) this is the canvas X. Zero before
113
+ /// first frame is accepted. JS-side band overlay computes
114
+ /// `fillRatio = paintedExtent / panExtent` to size the thumb.
115
+ @property (nonatomic, readonly) NSInteger paintedExtent;
116
+
117
+ /// V12.14.9 — total pan-axis extent of the canvas (the engine's
118
+ /// `_canvasPanExtent` config value, default 5000). Constant for
119
+ /// the lifetime of a capture. Emitted on every telemetry frame
120
+ /// for symmetry with `paintedExtent`; JS uses the ratio.
121
+ @property (nonatomic, readonly) NSInteger panExtent;
122
+ @end
123
+
124
+
125
+ /// V15 — paint-mode toggle for the slit-scan engine.
126
+ /// `RLISPaintModeFirstPaintedWins` preserves the first frame's content
127
+ /// (V13.0e+ default). `RLISPaintModeFeatherBlend` alpha-blends new
128
+ /// content into already-painted pixels at slit boundaries (V13.0d-style
129
+ /// row alpha ramp), aiming to smooth visible seams when many slits
130
+ /// stack with small per-accept advance.
131
+ typedef NS_ENUM(NSInteger, RLISPaintMode) {
132
+ RLISPaintModeFirstPaintedWins = 0,
133
+ RLISPaintModeFeatherBlend = 1,
134
+ };
135
+
136
+ /// V15.0c — where on the camera frame the per-accept sliver is taken
137
+ /// from. For a typical landscape vertical pan tilting DOWN, the LEADING
138
+ /// EDGE (new content not seen by previous frames) is at the BOTTOM of
139
+ /// the camera sensor frame; for upward tilt, the leading edge is at
140
+ /// the TOP. `Center` is the V13.x default (sliver from the centred
141
+ /// 70% / 30% of pan-axis).
142
+ typedef NS_ENUM(NSInteger, RLISSliverPosition) {
143
+ RLISSliverPositionCenter = 0,
144
+ RLISSliverPositionBottom = 1,
145
+ RLISSliverPositionTop = 2,
146
+ };
147
+
148
+ /// V15 — projection toggle for the hybrid engine.
149
+ /// `RLISHybridProjectionCylindrical` is the V12.x baseline; `Planar`
150
+ /// uses cv::detail::PlaneWarper, well-behaved for pans under ~60°.
151
+ typedef NS_ENUM(NSInteger, RLISHybridProjection) {
152
+ RLISHybridProjectionCylindrical = 0,
153
+ RLISHybridProjectionPlanar = 1,
154
+ };
155
+
156
+ /// V15.0d — source of the plane used by the slit-scan engine's V15.0b
157
+ /// plane-projected stitch path. Replaces V15.0b's boolean
158
+ /// `useDetectedPlane` toggle (which is kept as a deprecated alias)
159
+ /// with three explicit options:
160
+ ///
161
+ /// • `Disabled` — no plane projection; slit-scan path runs
162
+ /// (V13.x baseline + V15 refinements).
163
+ /// • `ARKitDetected` — use the first vertical plane that ARKit
164
+ /// finds AND whose surface normal aligns with
165
+ /// the camera's view direction (filter
166
+ /// threshold = `arkitPlaneAlignmentThreshold`).
167
+ /// If no aligned plane is detected, the engine
168
+ /// falls back to the slit-scan path silently.
169
+ /// • `Virtual` — synthesize a plane from the FIRST plane-
170
+ /// projected frame's camera pose:
171
+ /// origin = camera_pos + virtualPlaneDepthMeters
172
+ /// × camera_forward
173
+ /// normal = -camera_forward
174
+ /// No ARKit dependency; always available.
175
+ /// Loses the "real depth" advantage of an
176
+ /// ARKit-detected plane.
177
+ ///
178
+ /// Why both ARKit and Virtual exist:
179
+ /// Field testing showed ARKit plane detection often picks the WRONG
180
+ /// surface (side wall, doorframe, table edge) instead of the fixture
181
+ /// face the user is scanning — producing nonsense projections (huge
182
+ /// corner ray distances, 90°-rotated quads). Virtual side-steps this
183
+ /// with a synthetic plane perpendicular to the camera at first frame.
184
+ /// Operators can A/B between the two and pick whichever wins for
185
+ /// their typical scene.
186
+ typedef NS_ENUM(NSInteger, RLISPlaneSource) {
187
+ RLISPlaneSourceDisabled = 0,
188
+ RLISPlaneSourceARKitDetected = 1,
189
+ RLISPlaneSourceVirtual = 2,
190
+ };
191
+
192
+ /// V15.0g — how the plane-projection helper renders each frame onto
193
+ /// the canvas. Affects ARKitDetected and Virtual modes; ignored when
194
+ /// planeSource = Disabled.
195
+ ///
196
+ /// • `Trapezoidal` (V15.0b legacy):
197
+ /// Geometrically correct 3D mapping. Each camera pixel is
198
+ /// raycast onto the plane and pasted at the resulting plane-
199
+ /// local canvas position. When the camera tilts off-
200
+ /// perpendicular, the projected camera frame becomes a
201
+ /// TRAPEZOID — visually distorted (Ram observed cooler bottom
202
+ /// 2.3× wider than top at 30° tilt, 2026-05-08).
203
+ /// • `Rectified` (V15.0g default):
204
+ /// Camera frame is pasted as a CLEAN RECTANGLE around its
205
+ /// projected anchor on the canvas. Anchor is the canvas
206
+ /// position of the camera CENTER raycast. Rectangle size
207
+ /// depends on plane distance × pixels-per-meter. No
208
+ /// trapezoidal distortion regardless of tilt angle, at the
209
+ /// cost of true 3D-correctness (the camera's per-pixel
210
+ /// perspective is preserved within the rectangle, but
211
+ /// different tilts don't reconcile geometrically — they
212
+ /// overlap with FirstPaintedWins keeping the earliest paint).
213
+ ///
214
+ /// Field-validate Rectified vs Trapezoidal; the right choice depends
215
+ /// on the operator's typical pan range and tolerance for distortion.
216
+ typedef NS_ENUM(NSInteger, RLISPlaneProjectionStyle) {
217
+ RLISPlaneProjectionStyleTrapezoidal = 0,
218
+ RLISPlaneProjectionStyleRectified = 1,
219
+ };
220
+
221
+ /// V15 stitcher config — single source of truth for which correction
222
+ /// stages run in the slit-scan and hybrid engines. Each engine mode
223
+ /// (`hybrid`, `slitscan-rotate`, `slitscan-both`) has a default config
224
+ /// returned by `+configForMode:`; JS-side callers (settings UI, capture
225
+ /// start options) override individual fields on top of the default.
226
+ ///
227
+ /// V13.0e+/V13.0g/V14.0a correction stages are preserved in the source;
228
+ /// each is gated on the corresponding `enableX` flag. Field iteration
229
+ /// happens by toggling settings, not by recompiling.
230
+ @interface RLISStitcherConfig : NSObject
231
+
232
+ // ── Slit shaping (slit-scan engine only) ────────────────────────────
233
+
234
+ /// Fraction of the pan-axis the rectilinear slit retains per frame
235
+ /// (the rest is cropped equally from both edges). Range 0.10 – 0.70.
236
+ /// Default 0.30 for both slitscan modes; n/a for hybrid.
237
+ @property (nonatomic) double kPanAxisFractionRect;
238
+
239
+ /// Minimum pan-axis advance required before a frame is accepted.
240
+ /// 0 = accept on every consumeFrame (Apple-dense slit-scan); 50 =
241
+ /// V13.0g default. Default 0 for both slitscan modes; n/a for hybrid.
242
+ @property (nonatomic) NSInteger kMinAcceptDeltaPx;
243
+
244
+ // ── Per-stage correction toggles (slit-scan engine) ─────────────────
245
+
246
+ /// V13.0e+: ORB triangulation + median-Z parallax correction.
247
+ @property (nonatomic) BOOL enableTriangulation;
248
+ /// V13.0g: per-accept incremental Δt accumulator on top of triangulation.
249
+ @property (nonatomic) BOOL enableTriAccumulator;
250
+
251
+ /// V15 new: 1D NCC perpendicular-axis wobble correction (slitscan-rotate).
252
+ @property (nonatomic) BOOL enable1dNcc;
253
+ /// 1D NCC search radius in pixels (5 – 30).
254
+ @property (nonatomic) NSInteger nccSearchRadius1d;
255
+
256
+ /// V13.0g: 2D NCC fine-alignment after triangulation.
257
+ @property (nonatomic) BOOL enable2dNcc;
258
+ /// V15.0d: 2D NCC search half-window in pixels. Was a hardcoded
259
+ /// constexpr (V13.0g: 30, V15.0c.4: 12). Smaller = less wandering on
260
+ /// repetitive textures, but easier to miss the true overlap when pose
261
+ /// noise is high. Range 4 – 30. Default 12 for slitscan modes.
262
+ @property (nonatomic) NSInteger nccSearchMargin2d;
263
+ /// V15.0d: 2D NCC confidence threshold below which the correction
264
+ /// is rejected. Was hardcoded (V13.0g: 0.6, V15.0c.4: 0.75). Higher
265
+ /// = stricter — fewer false matches on repetitive textures, but more
266
+ /// frames where NCC silently doesn't fire. Range 0.4 – 0.95. Default
267
+ /// 0.75 for slitscan modes.
268
+ @property (nonatomic) double nccConfidenceThreshold2d;
269
+
270
+ /// V15.0d new (1B): EMA smoothing on 2D NCC corrections. When enabled,
271
+ /// the applied correction is `α × current + (1−α) × prev` instead of
272
+ /// just `current`. Dampens single-frame snaps to spurious peaks at
273
+ /// the cost of a 2-frame lag. Default OFF for slitscan modes.
274
+ @property (nonatomic) BOOL enableNcc2dEmaSmoothing;
275
+ /// V15.0d new: EMA weight on the CURRENT-frame NCC correction (the
276
+ /// remaining `1 − α` weight is on the previous correction). Range
277
+ /// 0.1 – 0.9. Default 0.4 (60% prev / 40% current — heavy damping).
278
+ @property (nonatomic) double ncc2dEmaAlpha;
279
+
280
+ /// V15.0d new (1C): pan-axis-aware 2D NCC. When enabled, the cross-
281
+ /// axis (perpendicular to the pan direction) NCC correction is
282
+ /// clamped to ±`ncc2dCrossAxisLockPx`, regardless of what the search
283
+ /// window size allows. Idea: 1D NCC already handles cross-axis
284
+ /// wobble; 2D NCC's cross-axis search is mostly noise. Default OFF.
285
+ @property (nonatomic) BOOL enableNcc2dPanAxisLock;
286
+ /// V15.0d new: cross-axis clamp for the pan-axis-aware mode. Range
287
+ /// 0 – 15 px. Default 5.
288
+ @property (nonatomic) NSInteger ncc2dCrossAxisLockPx;
289
+
290
+ /// V14.0a: RANSAC homography per slit + cv::warpPerspective.
291
+ @property (nonatomic) BOOL enableRansacHomography;
292
+
293
+ /// V15 new: paint mode for the slit-scan engine. Default
294
+ /// FirstPaintedWins for slitscan-rotate, FeatherBlend for slitscan-both.
295
+ @property (nonatomic) RLISPaintMode paintMode;
296
+
297
+ /// V15.0c new: where on the camera frame the per-accept sliver is
298
+ /// taken. Default Center (V13.x behaviour). Bottom = leading edge for
299
+ /// typical top-to-bottom landscape pan.
300
+ @property (nonatomic) RLISSliverPosition sliverPosition;
301
+
302
+ /// V15.0c new: when YES, the FIRST accepted frame paints the entire
303
+ /// camera frame at canvas (0, 0) instead of just the sliver. Subsequent
304
+ /// frames still use the configured sliver clip. Useful with sliverPosition=
305
+ /// Bottom: the first frame anchors the canvas with full-frame content,
306
+ /// then leading-edge slivers extend the canvas as the camera pans.
307
+ /// Default YES for slitscan-rotate / slitscan-both.
308
+ @property (nonatomic) BOOL firstFrameFullFrame;
309
+
310
+ /// **DEPRECATED in V15.0d** — use `planeSource` instead.
311
+ ///
312
+ /// V15.0b boolean toggle for the plane-projected stitch path. Kept
313
+ /// as an alias for backward compat: when `planeSource` is left at
314
+ /// its default (Disabled), `useDetectedPlane = YES` upgrades it to
315
+ /// `ARKitDetected`. New callers should set `planeSource` directly.
316
+ ///
317
+ /// V15.0b semantics: if YES, the slit-scan engine projects each
318
+ /// accepted frame onto a vertical plane. Composes with paint mode;
319
+ /// bypasses the slit-axis 2D refinements (triangulation, 2D NCC,
320
+ /// RANSAC homography) — those don't apply when the canvas is a
321
+ /// real 3D plane.
322
+ @property (nonatomic) BOOL useDetectedPlane;
323
+
324
+ /// V15.0d new: source of the plane used by the V15.0b path. See
325
+ /// `RLISPlaneSource` enum docs above for tradeoffs. Default
326
+ /// Disabled for all engine modes; settings UI / capture overrides
327
+ /// promote to ARKitDetected or Virtual.
328
+ @property (nonatomic) RLISPlaneSource planeSource;
329
+
330
+ /// V15.0d new: depth (metres) at which the synthetic plane is placed
331
+ /// in front of the camera when `planeSource = Virtual`. Set the
332
+ /// plane at the user's typical scan distance — too close = scene
333
+ /// content gets clipped behind the plane; too far = perspective
334
+ /// distortion grows. Range 0.3 – 5.0 m. Default 1.5 m.
335
+ @property (nonatomic) double virtualPlaneDepthMeters;
336
+
337
+ /// V15.0d new: minimum dot product between the candidate plane's
338
+ /// surface normal and the camera's negative-forward direction
339
+ /// (i.e. the direction the camera is facing). Used by
340
+ /// `RNSARSession.didAdd` to filter ARKit-detected planes for
341
+ /// `planeSource = ARKitDetected`. 1.0 = plane perfectly facing
342
+ /// camera; 0.0 = plane edge-on to camera; -1.0 = facing away.
343
+ /// Range 0.0 – 1.0. Default 0.6 (≈53° max angle off-camera).
344
+ @property (nonatomic) double arkitPlaneAlignmentThreshold;
345
+
346
+ /// V15.0g new: plane projection rendering style. See enum docs above
347
+ /// for tradeoffs (Trapezoidal = 3D-correct + distorted; Rectified =
348
+ /// clean-rectangle + slight 3D approximation). Ignored when
349
+ /// planeSource = Disabled. Default Rectified for slit-scan modes.
350
+ @property (nonatomic) RLISPlaneProjectionStyle planeProjectionStyle;
351
+
352
+ // ── Hybrid-specific ─────────────────────────────────────────────────
353
+
354
+ /// V15 new: projection for hybrid engine. Default Planar in V15
355
+ /// (was Cylindrical in V12.x – V14.0a).
356
+ @property (nonatomic) RLISHybridProjection hybridProjection;
357
+
358
+ /// Build a default config for the named engine mode.
359
+ /// Recognised modes: `@"hybrid"`, `@"slitscan-rotate"`,
360
+ /// `@"slitscan-both"`. Backward-compat: `@"firstwins-rectilinear"`
361
+ /// maps to `slitscan-rotate`; legacy `@"firstwins"` /
362
+ /// `@"firstwins-zoomed"` log a deprecation warning and fall back to
363
+ /// `slitscan-both`. Unrecognised modes default to `slitscan-both`.
364
+ + (instancetype)configForMode:(NSString *)mode;
365
+
366
+ @end
367
+
368
+
369
+ /// Snapshot of the current panorama canvas. Returned by `snapshot`.
370
+ @interface RLISSnapshot : NSObject
371
+ /// Path to the JPEG written for this snapshot. Lives in
372
+ /// `NSTemporaryDirectory()` and is overwritten on each snapshot —
373
+ /// the host is expected to consume it before requesting the next.
374
+ @property (nonatomic, copy, readonly) NSString *panoramaPath;
375
+ @property (nonatomic, readonly) NSInteger width;
376
+ @property (nonatomic, readonly) NSInteger height;
377
+ @property (nonatomic, readonly) NSInteger acceptedCount;
378
+ @end
379
+
380
+
381
+ @interface OpenCVIncrementalStitcher : NSObject
382
+
383
+ /// Initialise an engine ready to accept frames at the given compose
384
+ /// resolution. `composeWidth` and `composeHeight` are the dimensions
385
+ /// each ingested ARFrame is scaled to before feature extraction —
386
+ /// 720p (1280×720 landscape) is the design-doc default. Smaller =
387
+ /// faster + less memory at the cost of feature density.
388
+ ///
389
+ /// `canvasWidth` and `canvasHeight` size the pre-allocated panorama
390
+ /// canvas (CV_8UC3). Pick generously to avoid clipping long pans.
391
+ /// Defaults if 0/0 passed: 4800×1600 (≈23 MB). The first accepted
392
+ /// frame is placed in the canvas centre so growth in either pan
393
+ /// direction is symmetric.
394
+ - (instancetype)initWithComposeWidth:(NSInteger)composeWidth
395
+ composeHeight:(NSInteger)composeHeight
396
+ canvasWidth:(NSInteger)canvasWidth
397
+ canvasHeight:(NSInteger)canvasHeight
398
+ featherPx:(NSInteger)featherPx
399
+ frameRotationDegrees:(NSInteger)frameRotationDegrees NS_DESIGNATED_INITIALIZER;
400
+
401
+ - (instancetype)init NS_UNAVAILABLE;
402
+
403
+ /// V15 — set the per-stage correction config. Should be called once
404
+ /// after init, before any `ingestPixelBuffer:` call. If never called,
405
+ /// the engine uses a default equivalent to
406
+ /// `+[RLISStitcherConfig configForMode:@"hybrid"]`.
407
+ - (void)setConfig:(RLISStitcherConfig *)config;
408
+
409
+ /// Try to incorporate `pixelBuffer` into the running panorama.
410
+ ///
411
+ /// V6 (pose-driven): the engine builds the warp homography
412
+ /// `H = T · K · M · R_first⁻¹ · R_new · M · K⁻¹` directly from the
413
+ /// ARKit camera quaternion and intrinsics passed alongside the
414
+ /// frame. No feature extraction, no matching, no RANSAC — the
415
+ /// alignment is geometrically exact for the rotational pans that
416
+ /// dominate handheld panoramas. `M = diag(1, -1, -1)` converts
417
+ /// ARKit's (Y-up, -Z forward) camera frame to OpenCV's standard
418
+ /// (Y-down, +Z forward) frame.
419
+ ///
420
+ /// Pose-delta gating still uses (yaw, pitch, fov*Degrees) to skip
421
+ /// frames outside the overlap window before any warp work runs.
422
+ ///
423
+ /// `trackingPoor` should be YES when the AR session reports
424
+ /// non-tracking state at the time of this frame; the engine then
425
+ /// skips immediately with `RLISFrameOutcomeSkippedTrackingPoor`.
426
+ - (RLISFrameTelemetry *)ingestPixelBuffer:(CVPixelBufferRef)pixelBuffer
427
+ qx:(double)qx
428
+ qy:(double)qy
429
+ qz:(double)qz
430
+ qw:(double)qw
431
+ tx:(double)tx
432
+ ty:(double)ty
433
+ tz:(double)tz
434
+ fx:(double)fx
435
+ fy:(double)fy
436
+ cx:(double)cx
437
+ cy:(double)cy
438
+ imageWidth:(NSInteger)imageWidth
439
+ imageHeight:(NSInteger)imageHeight
440
+ yaw:(double)yaw
441
+ pitch:(double)pitch
442
+ fovHorizDegrees:(double)fovHorizDegrees
443
+ fovVertDegrees:(double)fovVertDegrees
444
+ trackingPoor:(BOOL)trackingPoor
445
+ NS_SWIFT_NAME(ingest(pixelBuffer:qx:qy:qz:qw:tx:ty:tz:fx:fy:cx:cy:imageWidth:imageHeight:yaw:pitch:fovHorizDegrees:fovVertDegrees:trackingPoor:));
446
+
447
+ /// Snapshot the current panorama as a JPEG (overwriting any previous
448
+ /// snapshot file). Cheap enough to call after each accepted frame
449
+ /// for live-preview UX. Returns nil with `error` populated if the
450
+ /// snapshot failed (disk full, permission, etc.).
451
+ - (nullable RLISSnapshot *)snapshotWithJpegQuality:(NSInteger)quality
452
+ error:(NSError **)error;
453
+
454
+ /// Final write at end of capture — same shape as `snapshot` but
455
+ /// written to `outputPath` (caller-controlled location). Includes
456
+ /// a tight crop to the actual panorama bounds (no trailing canvas
457
+ /// black). After this call, the canvas is reset; the engine is
458
+ /// ready for a fresh capture without re-init.
459
+ - (nullable RLISSnapshot *)finalizeAtPath:(NSString *)outputPath
460
+ jpegQuality:(NSInteger)quality
461
+ error:(NSError **)error;
462
+
463
+ /// Reset state so the engine can begin a new capture. Called
464
+ /// automatically by `finalizeAtPath:` and on construction.
465
+ - (void)reset;
466
+
467
+ /// Frames accepted into the panorama since `reset`. Read-only;
468
+ /// monotonically increasing within a capture.
469
+ @property (nonatomic, readonly) NSInteger acceptedCount;
470
+
471
+ @end
472
+
473
+ NS_ASSUME_NONNULL_END