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,240 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // keyframe_gate.hpp — shared C++ port of KeyframeGate.swift.
4
+ //
5
+ // Why a shared port:
6
+ // The Swift KeyframeGate has been the production-quality V16-Phase-0
7
+ // gate on iOS for months. The Android side was running a frame-
8
+ // counter MVP placeholder, producing different keyframe sets and
9
+ // therefore different panoramas across platforms. Porting to
10
+ // shared C++ that both iOS (via Obj-C++ bridge) and Android (via
11
+ // JNI) call into eliminates the divergence and makes panorama
12
+ // composition platform-identical.
13
+ //
14
+ // Algorithm summary (1:1 with KeyframeGate.swift comments):
15
+ // For each candidate frame, project its 4 image corners onto the
16
+ // latched ARKit/ARCore plane via ray-plane intersection. Compute
17
+ // convex-polygon overlap with the previous accepted keyframe's
18
+ // plane-projected polygon via Sutherland-Hodgman clipping.
19
+ // new_content_fraction = 1 − intersection_area / current_frame_area.
20
+ // Accept iff new_content_fraction ≥ overlapThreshold (default 0.4)
21
+ // and acceptedCount < maxCount (default 6).
22
+ //
23
+ // No-plane fallback:
24
+ // When the host can't supply a plane (planeSource=Disabled, or
25
+ // plane lock hasn't latched yet), the gate falls back to comparing
26
+ // the camera-forward angular delta from the last accepted keyframe.
27
+ // new_content = angularDelta / min(fovH, fovV). Same accept rule.
28
+ //
29
+ // First/last frames:
30
+ // - First frame is always accepted (anchor).
31
+ // - markNextFrameAsLast() arms a one-shot "next frame is the
32
+ // trailing keyframe, force-accept". Set on shutter-release path
33
+ // so we don't truncate the right edge of the scan.
34
+ //
35
+ // Threading: NOT thread-safe. Caller must serialise evaluate() /
36
+ // reset() / markNextFrameAsLast() / setters. On both platforms this
37
+ // is already guaranteed by the engine's work queue serial dispatch.
38
+
39
+ #pragma once
40
+
41
+ #include <cstdint>
42
+ #include "ar_frame_pose.h"
43
+
44
+ namespace retailens {
45
+
46
+ /// Strategy selector — chooses how the gate measures "new content" for
47
+ /// the accept decision. Set via `setStrategy(...)` between captures;
48
+ /// not safe to flip mid-capture.
49
+ ///
50
+ /// Pose — the original V16-Phase-0 algorithm: project frame corners
51
+ /// onto the latched plane, compare polygon overlap. Falls
52
+ /// back to camera-forward angular delta when projection
53
+ /// degenerates (no plane / behind-camera intersection).
54
+ /// Cheap but oversensitive when the latched plane covers a
55
+ /// small fraction of the visible frame: 6 cm of physical
56
+ /// motion at 2.7 m perpDist on a 0.4×1.6 m plane produced 6
57
+ /// accepts in 1 s (Ram report 2026-05-13).
58
+ ///
59
+ /// Flow — V16 fix-attempt-8/A2: sparse Lucas-Kanade optical flow.
60
+ /// Detect Shi-Tomasi corners once per accepted keyframe;
61
+ /// track them into each incoming frame with
62
+ /// `calcOpticalFlowPyrLK`; accept when the median pan-axis
63
+ /// displacement crosses `overlapThreshold * frame_dim` (the
64
+ /// same 0.40 threshold as Pose, with directly-translatable
65
+ /// semantics: 40 % of frame dim = 40 % new content). Costs
66
+ /// one detect (~15–25 ms) per accept + one track (~1–3 ms)
67
+ /// per frame. Scale-invariant — independent of plane size.
68
+ /// Falls back to angular delta when feature tracking fails
69
+ /// (texture-poor scene / motion exceeds pyramid window).
70
+ ///
71
+ /// Default is `Pose` to keep behaviour unchanged when this field
72
+ /// arrives unset. The TS/host side flips to `Flow` via settings in a
73
+ /// follow-up commit.
74
+ enum class GateStrategy : int32_t {
75
+ Pose = 0,
76
+ Flow = 1,
77
+ };
78
+
79
+ /// 1:1 with KeyframeGate.swift's `reason` strings. An int enum
80
+ /// crosses the bridge cleanly; iOS/Android wrappers map back to
81
+ /// strings for telemetry.
82
+ enum class KeyframeGateDecisionReason : int32_t {
83
+ // Accept reasons
84
+ AcceptDisabled = 0, // "gate-disabled" — pass-through when !enabled
85
+ AcceptForceLast = 1, // "force-last" — shutter-release force-accept
86
+ AcceptFirstOnPlane = 2, // "first-anchored-on-plane"
87
+ AcceptFirstNoPlane = 3, // "first-no-plane"
88
+ AcceptOk = 4, // "ok" — plane path
89
+ AcceptOkAngular = 5, // "ok-angular" — no-plane fallback
90
+ AcceptProjectionDegenerate = 6, // "projection-degenerate"
91
+ AcceptCurrentAreaZero = 7, // "current-area-zero"
92
+ AcceptNoPoseYet = 8, // "no-pose-yet" — defensive
93
+ // Reject reasons
94
+ RejectMaxReached = 9, // "max-reached"
95
+ RejectOverlapTooHigh = 10, // "overlap-too-high"
96
+ RejectOverlapTooHighAngular = 11, // "overlap-too-high (angular)"
97
+ // Flow strategy reasons (V16 A2)
98
+ AcceptOkFlow = 12, // "ok-flow" — flow displacement-percentile crossed threshold
99
+ AcceptFirstFlow = 13, // "first-flow" — first frame under flow strategy
100
+ RejectOverlapTooHighFlow = 14, // "overlap-too-high (flow)"
101
+ AcceptFlowTranslation = 15, // "ok-flow-translation" — translation since last accept exceeded flowMaxTranslationM (force-accept even when novelty < threshold)
102
+ };
103
+
104
+ struct KeyframeGateDecision {
105
+ bool accept;
106
+ KeyframeGateDecisionReason reason;
107
+ double newContentFraction; // -1.0 when not computed (disabled / first / force-last)
108
+ int32_t acceptedCount;
109
+ int32_t maxCount;
110
+ };
111
+
112
+ /// Opaque handle — implementation kept in keyframe_gate.cpp via pImpl.
113
+ /// Lifetime is owned by the host wrapper (Obj-C++ object on iOS, JNI
114
+ /// `Long` handle on Android).
115
+ class KeyframeGate {
116
+ public:
117
+ KeyframeGate();
118
+ ~KeyframeGate();
119
+
120
+ // Non-copyable, non-movable — the pImpl is heap-owned and the
121
+ // bridges manage lifetime explicitly.
122
+ KeyframeGate(const KeyframeGate&) = delete;
123
+ KeyframeGate& operator=(const KeyframeGate&) = delete;
124
+ KeyframeGate(KeyframeGate&&) = delete;
125
+ KeyframeGate& operator=(KeyframeGate&&) = delete;
126
+
127
+ // ── Settings (called between captures, not per-frame) ─────────
128
+ void setEnabled(bool enabled);
129
+ void setOverlapThreshold(double threshold); // [0, 1]; default 0.4
130
+ void setMaxCount(int32_t maxCount); // ≥ 1; default 6
131
+ void markNextFrameAsLast(); // one-shot, consumed by next evaluate()
132
+ void reset(); // clears acceptedCount, lastCorners, planeCached AND flow state
133
+
134
+ // ── Strategy selector + Flow params (V16 A2) ──────────────────
135
+ // Flow params are only consulted when strategy == Flow. Safe to
136
+ // set when strategy == Pose; they'll be live the moment strategy
137
+ // flips. Defaults below are stable on iPhone 13/14/15 testing.
138
+ void setStrategy(GateStrategy strategy);
139
+ GateStrategy getStrategy() const;
140
+ void setFlowMaxCorners(int32_t maxCorners); // ≥ 30; default 150
141
+ void setFlowQualityLevel(double quality); // (0, 1]; default 0.01
142
+ void setFlowMinDistance(double minDistance); // ≥ 1.0; default 10.0 (working-resolution pixels)
143
+ /// V16 — translation budget for the Flow strategy. When the camera's
144
+ /// 3D Euclidean translation since the last accepted keyframe exceeds
145
+ /// this value (metres), the gate force-accepts the current frame
146
+ /// even if novelty < `overlapThreshold`. Purpose: prevent the
147
+ /// upstream stitcher's matcher from being fed two views with so
148
+ /// much parallax that even an affine match-confidence collapses
149
+ /// (Ram report 2026-05-13: captures with 25-60 cm of camera
150
+ /// translation between keyframes produced validPairs=0 even after
151
+ /// the matcher swap to AffineBestOf2NearestMatcher). Default
152
+ /// 0.0 = disabled (back-compat). Sensible production setting:
153
+ /// 0.08 (8 cm). Clamped to ≥ 0.0.
154
+ void setFlowMaxTranslationM(double metres);
155
+ /// V16 — percentile (in [0.5, 0.99]) used to aggregate the tracked
156
+ /// features' absolute displacements into a per-axis novelty estimate.
157
+ /// Default 0.85. Pre-V16 used median (0.50); the median under-
158
+ /// reports novelty when the user has rotated the camera enough that
159
+ /// the LEADING EDGE of new content is visible but most-existing-
160
+ /// features have moved less than half a frame. 85th-percentile picks
161
+ /// up the leading-edge motion sooner and lines up better with the
162
+ /// user's visual perception of "new content visible". Clamped to
163
+ /// [0.5, 0.99].
164
+ void setFlowNoveltyPercentile(double percentile);
165
+
166
+ /// 2026-05-14 — disable the angular-delta fallback that the gate
167
+ /// otherwise uses when (a) the pose-strategy's plane-projection
168
+ /// is unavailable / degenerate, or (b) the flow-strategy's KLT
169
+ /// tracking fails. When `true`, every angular-fallback path
170
+ /// returns `RejectOverlapTooHighAngular` regardless of the actual
171
+ /// pose, so the only path that can accept a frame is the strategy's
172
+ /// primary signal (plane-overlap for Pose, flow-displacement for
173
+ /// Flow).
174
+ ///
175
+ /// Set this to `true` in non-AR mode (captureSource ∈ {wide,
176
+ /// ultrawide}) where pose data is missing / IMU-derived — the
177
+ /// angular calc would produce nonsense in that environment.
178
+ /// Default `false` (back-compat — AR mode uses the fallback).
179
+ void setDisableAngularFallback(bool disabled);
180
+
181
+ // ── Per-frame evaluation ──────────────────────────────────────
182
+ //
183
+ // Two overloads:
184
+ //
185
+ // evaluate(pose, plane)
186
+ // Backward-compat entry point. Used by callers that don't
187
+ // (yet) supply per-frame image data. Always runs the Pose
188
+ // strategy regardless of `getStrategy()` — Flow needs the
189
+ // image to compute novelty, so Pose is the only thing it
190
+ // CAN do here. Android JNI today calls this; the iOS side
191
+ // moves to `evaluateWithFrame` in commit 2.
192
+ //
193
+ // evaluateWithFrame(pose, plane, grayData, width, height, stride)
194
+ // Strategy-aware entry point. When strategy == Flow, runs
195
+ // sparse-flow novelty on the supplied grayscale frame.
196
+ // When strategy == Pose, behaves identically to `evaluate`
197
+ // (the frame data is ignored — no extra cost beyond the
198
+ // caller's pixel-buffer → grayscale conversion, which the
199
+ // caller can elide by checking strategy first).
200
+ //
201
+ // @param pose camera pose + intrinsics for the frame
202
+ // @param latchedPlane optional plane transform (column-major 4×4
203
+ // matching ARKit ARPlaneAnchor convention).
204
+ // Pass nullptr if no plane is latched →
205
+ // gate uses angular-delta fallback.
206
+ // @param grayData pointer to grayscale 8-bit pixel data.
207
+ // Non-owning; data only needs to be valid
208
+ // for the duration of this call.
209
+ // @param width/height frame dimensions in pixels.
210
+ // @param stride bytes per row (usually equal to width;
211
+ // larger when the underlying buffer is
212
+ // padded).
213
+ KeyframeGateDecision evaluate(const Pose& pose,
214
+ const PlaneTransform* latchedPlane);
215
+ KeyframeGateDecision evaluateWithFrame(const Pose& pose,
216
+ const PlaneTransform* latchedPlane,
217
+ const uint8_t* grayData,
218
+ int32_t width,
219
+ int32_t height,
220
+ int32_t stride);
221
+
222
+ // ── State accessors (read-only, post-evaluate) ────────────────
223
+ int32_t getAcceptedCount() const;
224
+ int32_t getMaxCount() const;
225
+ bool isEnabled() const;
226
+
227
+ private:
228
+ struct Impl;
229
+ Impl* pImpl_;
230
+
231
+ // Shared angular-delta evaluation path. Used by §4 (no plane was
232
+ // ever latched) and §5's degenerate branches (V16 Phase 2 fix —
233
+ // projection-degenerate / current-area-zero fall back here rather
234
+ // than accepting blindly, which used to burst-accept every frame
235
+ // and corrupt the gate cap).
236
+ static KeyframeGateDecision evaluateAngularFallback(
237
+ Impl& s, const Pose& pose);
238
+ };
239
+
240
+ } // namespace retailens