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,328 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // KeyframeGate — Swift facade over the shared C++ KeyframeGate.
4
+ //
5
+ // This file used to BE the algorithm (~545 lines of Swift simd math).
6
+ // As of P3-B of the Android-iOS parity work, the algorithm lives in
7
+ // retailens-capture-sdk/cpp/keyframe_gate.{hpp,cpp} and is shared with
8
+ // the Android side via JNI. This Swift class is now a thin facade
9
+ // that:
10
+ //
11
+ // 1. Preserves the original public Swift API exactly so the 13
12
+ // callsites in IncrementalStitcher.swift don't change.
13
+ // 2. Marshals the Swift `RNSARFramePose` + `simd_float4x4?`
14
+ // into the primitive types the Obj-C++ bridge expects.
15
+ // 3. Maps the Obj-C++ bridge's return shape back into the original
16
+ // `KeyframeGateDecision` struct.
17
+ //
18
+ // Why a facade (not direct callsite rewrites):
19
+ // The Swift code has 13 callsites — each touching enabled,
20
+ // overlapThreshold, maxCount, forceAcceptNext, reset(), evaluate(...),
21
+ // acceptedCount. Rewriting them all introduces churn and regression
22
+ // risk. A drop-in facade keeps the call shape identical; the only
23
+ // change to the engine code is "underneath, this calls C++ instead
24
+ // of Swift simd math". iOS device-verify (P3-C) will confirm
25
+ // behaviour is bit-identical to the old Swift impl.
26
+ //
27
+ // The original Swift implementation is preserved at
28
+ // /tmp/KeyframeGate.swift.bak for ~one session; can be retrieved
29
+ // from git history thereafter (commit before P3-B).
30
+
31
+ import Foundation
32
+ import simd
33
+
34
+ /// Returned from `KeyframeGate.evaluate(pose:latchedPlane:)`. Same
35
+ /// shape as the original Swift definition so callers don't change.
36
+ struct KeyframeGateDecision {
37
+ let accept: Bool
38
+ /// Short reason string for fault-level logging and JS telemetry.
39
+ /// 1:1 mapping with the C++ `KeyframeGateDecisionReason` enum is
40
+ /// done in `KeyframeGateBridge.mm::kReasonStringFor`.
41
+ let reason: String
42
+ /// Computed new-content fraction in [0, 1]. -1.0 if not computed
43
+ /// (gate disabled, force-first/last, no plane available).
44
+ let newContentFraction: Double
45
+ /// Keyframes accepted so far (including this one if accept=true).
46
+ let acceptedCount: Int
47
+ /// Max keyframes for the capture (0 if gate disabled).
48
+ let maxCount: Int
49
+ }
50
+
51
+ final class KeyframeGate {
52
+
53
+ // The Obj-C++ bridge owns the C++ `retailens::KeyframeGate`
54
+ // instance. We keep a single instance per KeyframeGate Swift
55
+ // object — lifetimes are tied (ARC dealloc → bridge dealloc → C++
56
+ // destructor).
57
+ private let bridge = KeyframeGateBridge()
58
+
59
+ // MARK: - Settings (called between captures)
60
+ //
61
+ // All settings are pass-throughs to the C++ instance via the
62
+ // bridge. We use computed getters that read fresh from the
63
+ // bridge so the value is never out-of-sync with the C++ state
64
+ // (would matter if a caller mutates the bridge directly — they
65
+ // don't, but defensive).
66
+
67
+ /// True when frameSelectionMode == "pose-based". When false,
68
+ /// every evaluate() returns accept=true (gate is a passthrough).
69
+ var enabled: Bool {
70
+ get { bridge.isEnabled() }
71
+ set { bridge.setEnabled(newValue) }
72
+ }
73
+
74
+ /// Required new-content fraction (0…1). Default 0.4 (40% new
75
+ /// content ≈ 4-5 keyframes for a 90° landscape pan).
76
+ ///
77
+ /// NOTE: This is stored locally too because the C++ bridge has no
78
+ /// getter for it (the read-side was never needed by Swift code).
79
+ /// Setter is what propagates into C++.
80
+ var overlapThreshold: Double = 0.4 {
81
+ didSet { bridge.setOverlapThreshold(overlapThreshold) }
82
+ }
83
+
84
+ /// Hard cap on keyframes per capture (default 6). Same getter
85
+ /// pattern as enabled — we read from the bridge.
86
+ var maxCount: Int {
87
+ get { bridge.maxCount() }
88
+ set { bridge.setMaxCount(newValue) }
89
+ }
90
+
91
+ // MARK: - V16 A2 — strategy + flow tunables
92
+
93
+ /// Mirror of `retailens::GateStrategy`. Pose = the V16 Phase-0
94
+ /// plane-overlap path. Flow = V16 A2 sparse-optical-flow novelty
95
+ /// (needs per-frame image data via the `evaluate(pose:plane:pixelBuffer:)`
96
+ /// overload below; the older pixel-buffer-free `evaluate(pose:plane:)`
97
+ /// silently falls back to Pose regardless of this setting).
98
+ enum GateStrategy: Int {
99
+ case pose = 0
100
+ case flow = 1
101
+ }
102
+
103
+ var strategy: GateStrategy {
104
+ get { GateStrategy(rawValue: bridge.strategy().rawValue) ?? .pose }
105
+ set { bridge.setStrategy(KGBStrategy(rawValue: newValue.rawValue) ?? .pose) }
106
+ }
107
+
108
+ /// Flow strategy — number of Shi-Tomasi corners to detect per
109
+ /// accepted keyframe. Default 150; the C++ side clamps to ≥30.
110
+ /// Higher = more robust median, slower detect (~15-25 ms at 150
111
+ /// on iPhone 13 Pro). Tunable from PanoramaSettingsModal.
112
+ var flowMaxCorners: Int = 150 {
113
+ didSet { bridge.setFlowMaxCorners(flowMaxCorners) }
114
+ }
115
+
116
+ /// Flow strategy — Shi-Tomasi quality level (0, 1]. Default
117
+ /// 0.01. Lower = more (weaker) corners detected; higher = fewer
118
+ /// (stronger) corners. C++ side clamps to (0.001, 1.0].
119
+ var flowQualityLevel: Double = 0.01 {
120
+ didSet { bridge.setFlowQualityLevel(flowQualityLevel) }
121
+ }
122
+
123
+ /// Flow strategy — minimum pixel distance between detected
124
+ /// corners (in WORKING-resolution pixels — gate downscales to
125
+ /// 720 px longest side internally). Default 10. Higher =
126
+ /// more spatially-spread features = more representative median.
127
+ var flowMinDistance: Double = 10.0 {
128
+ didSet { bridge.setFlowMinDistance(flowMinDistance) }
129
+ }
130
+
131
+ /// V16 — translation budget for the Flow strategy, in CENTIMETRES
132
+ /// (UI-friendly unit; bridge converts to metres before crossing).
133
+ /// When > 0, the gate force-accepts a frame if the camera has
134
+ /// translated more than this distance (3D Euclidean) since the
135
+ /// last accepted keyframe — even when novelty < overlapThreshold.
136
+ /// Bounds the parallax between adjacent keyframes so the
137
+ /// downstream stitcher's matcher (even the affine one) sees
138
+ /// inputs it can handle. Default 0 = disabled (back-compat);
139
+ /// operator opts-in via the capture-settings UI. Recommended
140
+ /// starting value once enabled: 8 cm.
141
+ var flowMaxTranslationCm: Double = 0.0 {
142
+ didSet {
143
+ // Bridge takes metres; UI surface uses cm for legibility.
144
+ bridge.setFlowMaxTranslationM(flowMaxTranslationCm / 100.0)
145
+ }
146
+ }
147
+
148
+ /// V16 — percentile used to aggregate the tracked features'
149
+ /// absolute displacements into a per-axis novelty estimate.
150
+ /// Default 0.85. Pre-V16 used median (0.50); the higher
151
+ /// percentile picks up leading-edge motion sooner — better
152
+ /// matches user perception of "new content visible". Clamped
153
+ /// at the bridge to [0.5, 0.99].
154
+ var flowNoveltyPercentile: Double = 0.85 {
155
+ didSet { bridge.setFlowNoveltyPercentile(flowNoveltyPercentile) }
156
+ }
157
+
158
+ /// V16 — Swift-side throttle for gate evaluation cadence. Default
159
+ /// 1 (evaluate on every consumeFrame). Set higher to skip evals
160
+ /// on (N-1)/N frames — pure CPU/battery savings, no change to
161
+ /// WHICH frames are accepted (still subject to overlapThreshold
162
+ /// + translation budget). Range 1-10 enforced at start() time.
163
+ ///
164
+ /// This knob lives in Swift, not C++, because it's about HOW
165
+ /// OFTEN we call into the gate, not about gate-internal logic.
166
+ /// Read by IncrementalStitcher.consumeFrame before
167
+ /// invoking `evaluate(...)`.
168
+ var flowEvalEveryNFrames: Int = 1
169
+
170
+
171
+ /// One-shot flag: when set to `true`, the very next evaluate()
172
+ /// accepts unconditionally and the flag self-resets. Set by JS
173
+ /// shutter-release path so we don't truncate the trailing edge
174
+ /// of the scan.
175
+ ///
176
+ /// Lives only on the C++ side — the Swift `var` is a "write-only
177
+ /// trigger". Reading after the assignment will always see false
178
+ /// because the trigger is consumed inside the bridge. The
179
+ /// original Swift KeyframeGate also exposed it as a stored bool;
180
+ /// no caller reads the value, only writes it.
181
+ var forceAcceptNext: Bool {
182
+ get { false }
183
+ set {
184
+ if newValue {
185
+ bridge.markNextFrameAsLast()
186
+ }
187
+ }
188
+ }
189
+
190
+ // MARK: - State (read-only post-evaluate)
191
+
192
+ /// Keyframes accepted so far in this capture.
193
+ var acceptedCount: Int { bridge.acceptedCount() }
194
+
195
+ // MARK: - Lifecycle
196
+
197
+ func reset() {
198
+ bridge.reset()
199
+ // Re-apply Swift-side default settings that the bridge default-
200
+ // initializes too, but write through anyway in case the caller
201
+ // tweaked them — this guarantees the threshold survives reset.
202
+ bridge.setOverlapThreshold(overlapThreshold)
203
+ }
204
+
205
+ // MARK: - Evaluation
206
+
207
+ /// Decide whether to accept this ARFrame as a keyframe.
208
+ ///
209
+ /// Same call shape as the original Swift gate so callers in
210
+ /// IncrementalStitcher.swift don't change. Internally
211
+ /// marshals the pose + optional plane matrix into the Obj-C++
212
+ /// bridge's primitive args, calls into shared C++, then unwraps
213
+ /// the result.
214
+ func evaluate(
215
+ pose: RNSARFramePose,
216
+ latchedPlane: simd_float4x4?
217
+ ) -> KeyframeGateDecision {
218
+ // Flatten the 4×4 plane matrix into a 16-element NSNumber
219
+ // array column-major. simd_float4x4's `columns` property
220
+ // already gives us column-major access; passing in the order
221
+ // (column 0 elements, column 1 elements, …) matches what the
222
+ // C++ `PlaneTransform.m[16]` expects.
223
+ let plane16: [NSNumber]?
224
+ if let m = latchedPlane {
225
+ let c0 = m.columns.0
226
+ let c1 = m.columns.1
227
+ let c2 = m.columns.2
228
+ let c3 = m.columns.3
229
+ plane16 = [
230
+ NSNumber(value: c0.x), NSNumber(value: c0.y),
231
+ NSNumber(value: c0.z), NSNumber(value: c0.w),
232
+ NSNumber(value: c1.x), NSNumber(value: c1.y),
233
+ NSNumber(value: c1.z), NSNumber(value: c1.w),
234
+ NSNumber(value: c2.x), NSNumber(value: c2.y),
235
+ NSNumber(value: c2.z), NSNumber(value: c2.w),
236
+ NSNumber(value: c3.x), NSNumber(value: c3.y),
237
+ NSNumber(value: c3.z), NSNumber(value: c3.w),
238
+ ]
239
+ } else {
240
+ plane16 = nil
241
+ }
242
+
243
+ let result = bridge.evaluate(
244
+ withTx: Float(pose.tx),
245
+ ty: Float(pose.ty),
246
+ tz: Float(pose.tz),
247
+ qx: Float(pose.qx),
248
+ qy: Float(pose.qy),
249
+ qz: Float(pose.qz),
250
+ qw: Float(pose.qw),
251
+ fx: Float(pose.fx),
252
+ fy: Float(pose.fy),
253
+ cx: Float(pose.cx),
254
+ cy: Float(pose.cy),
255
+ imageWidth: Int32(pose.imageWidth),
256
+ imageHeight: Int32(pose.imageHeight),
257
+ plane16: plane16
258
+ )
259
+
260
+ return KeyframeGateDecision(
261
+ accept: result.accept,
262
+ reason: result.reasonString,
263
+ newContentFraction: result.newContentFraction,
264
+ acceptedCount: result.acceptedCount,
265
+ maxCount: result.maxCount
266
+ )
267
+ }
268
+
269
+ /// V16 A2 — strategy-aware evaluate that also accepts the
270
+ /// frame's pixel buffer. Required by `strategy = .flow` (sparse-
271
+ /// flow novelty needs the image content). When `strategy = .pose`
272
+ /// the pixel buffer is ignored — same result + cost as
273
+ /// `evaluate(pose:latchedPlane:)`.
274
+ ///
275
+ /// The bridge handles all pixel-format handling (YUV-direct,
276
+ /// BGRA-via-cvtColor, unknown → pose fallback).
277
+ func evaluate(
278
+ pose: RNSARFramePose,
279
+ latchedPlane: simd_float4x4?,
280
+ pixelBuffer: CVPixelBuffer
281
+ ) -> KeyframeGateDecision {
282
+ let plane16: [NSNumber]?
283
+ if let m = latchedPlane {
284
+ let c0 = m.columns.0
285
+ let c1 = m.columns.1
286
+ let c2 = m.columns.2
287
+ let c3 = m.columns.3
288
+ plane16 = [
289
+ NSNumber(value: c0.x), NSNumber(value: c0.y),
290
+ NSNumber(value: c0.z), NSNumber(value: c0.w),
291
+ NSNumber(value: c1.x), NSNumber(value: c1.y),
292
+ NSNumber(value: c1.z), NSNumber(value: c1.w),
293
+ NSNumber(value: c2.x), NSNumber(value: c2.y),
294
+ NSNumber(value: c2.z), NSNumber(value: c2.w),
295
+ NSNumber(value: c3.x), NSNumber(value: c3.y),
296
+ NSNumber(value: c3.z), NSNumber(value: c3.w),
297
+ ]
298
+ } else {
299
+ plane16 = nil
300
+ }
301
+
302
+ let result = bridge.evaluate(
303
+ pixelBuffer: pixelBuffer,
304
+ tx: Float(pose.tx),
305
+ ty: Float(pose.ty),
306
+ tz: Float(pose.tz),
307
+ qx: Float(pose.qx),
308
+ qy: Float(pose.qy),
309
+ qz: Float(pose.qz),
310
+ qw: Float(pose.qw),
311
+ fx: Float(pose.fx),
312
+ fy: Float(pose.fy),
313
+ cx: Float(pose.cx),
314
+ cy: Float(pose.cy),
315
+ imageWidth: Int32(pose.imageWidth),
316
+ imageHeight: Int32(pose.imageHeight),
317
+ plane16: plane16
318
+ )
319
+
320
+ return KeyframeGateDecision(
321
+ accept: result.accept,
322
+ reason: result.reasonString,
323
+ newContentFraction: result.newContentFraction,
324
+ acceptedCount: result.acceptedCount,
325
+ maxCount: result.maxCount
326
+ )
327
+ }
328
+ }
@@ -0,0 +1,141 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // KeyframeGateBridge.h — Obj-C++ wrapper exposing the shared C++
4
+ // KeyframeGate (in retailens-capture-sdk/cpp/) to Swift.
5
+ //
6
+ // Why this exists:
7
+ // The pose-driven keyframe-selection algorithm is the single most
8
+ // important quality-determining piece of the panorama pipeline.
9
+ // Historically it lived in pure Swift (KeyframeGate.swift), which
10
+ // meant the Android side had to either re-implement it (parity
11
+ // risk — confirmed bug in the V16 frame-counter MVP placeholder)
12
+ // or skip it. We've now ported the algorithm to shared C++ in
13
+ // cpp/keyframe_gate.{hpp,cpp}; this Obj-C++ bridge is the thin
14
+ // shim that lets Swift call into the same C++ code that the JNI
15
+ // side will call on Android.
16
+ //
17
+ // Threading:
18
+ // The C++ KeyframeGate is NOT thread-safe. Caller (Swift) must
19
+ // serialise — typically via the engine's workQueue. Same
20
+ // contract as the Swift-only KeyframeGate had before.
21
+
22
+ #import <Foundation/Foundation.h>
23
+ #import <CoreVideo/CVPixelBuffer.h>
24
+
25
+ NS_ASSUME_NONNULL_BEGIN
26
+
27
+ /// Mirror of `retailens::GateStrategy` (keyframe_gate.hpp). Bridged as
28
+ /// raw NSInteger across Obj-C; the Swift facade lifts it to an enum.
29
+ /// MUST stay 1:1 with the C++ enum integer values.
30
+ typedef NS_ENUM(NSInteger, KGBStrategy) {
31
+ KGBStrategyPose = 0, ///< Plane-projection-overlap path (default)
32
+ KGBStrategyFlow = 1, ///< Sparse-optical-flow novelty (V16 A2)
33
+ };
34
+
35
+ /// Mirror of `retailens::KeyframeGateDecision` in keyframe_gate.hpp.
36
+ /// `reasonCode` is the raw int32 of the C++ enum; `reasonString` is
37
+ /// the human-readable label matching the original Swift telemetry
38
+ /// strings (so JS telemetry stays bit-identical).
39
+ NS_SWIFT_NAME(KeyframeGateBridgeDecision)
40
+ @interface KGBDecision : NSObject
41
+ @property (nonatomic, readonly) BOOL accept;
42
+ @property (nonatomic, readonly) NSInteger reasonCode;
43
+ @property (nonatomic, readonly) NSString *reasonString;
44
+ @property (nonatomic, readonly) double newContentFraction;
45
+ @property (nonatomic, readonly) NSInteger acceptedCount;
46
+ @property (nonatomic, readonly) NSInteger maxCount;
47
+ @end
48
+
49
+ /// Thin Obj-C++ wrapper around `retailens::KeyframeGate`. All
50
+ /// methods are 1:1 with the C++ API except `evaluate…`, which
51
+ /// flattens the Swift call shape (pose struct + optional plane
52
+ /// matrix) into primitive C-callable args.
53
+ NS_SWIFT_NAME(KeyframeGateBridge)
54
+ @interface KeyframeGateBridge : NSObject
55
+
56
+ - (instancetype)init;
57
+
58
+ // ── Settings ────────────────────────────────────────────────────
59
+ - (void)setEnabled:(BOOL)enabled;
60
+ - (void)setOverlapThreshold:(double)threshold;
61
+ - (void)setMaxCount:(NSInteger)maxCount;
62
+ - (void)markNextFrameAsLast;
63
+ - (void)reset;
64
+
65
+ // ── Strategy + Flow params (V16 A2) ──────────────────────────────
66
+ - (void)setStrategy:(KGBStrategy)strategy;
67
+ - (KGBStrategy)strategy;
68
+ - (void)setFlowMaxCorners:(NSInteger)maxCorners;
69
+ - (void)setFlowQualityLevel:(double)quality;
70
+ - (void)setFlowMinDistance:(double)minDistance;
71
+ /// V16 — translation budget (metres). Set > 0 to force-accept on
72
+ /// translation overflow even when novelty < threshold; 0 disables.
73
+ /// See KeyframeGate.swift for the operator-facing description.
74
+ - (void)setFlowMaxTranslationM:(double)metres;
75
+ /// V16 — novelty aggregation percentile [0.5, 0.99]. Default 0.85.
76
+ /// See KeyframeGate.swift for the operator-facing description.
77
+ - (void)setFlowNoveltyPercentile:(double)percentile;
78
+
79
+ // ── Read-only state ─────────────────────────────────────────────
80
+ - (BOOL)isEnabled;
81
+ - (NSInteger)acceptedCount;
82
+ - (NSInteger)maxCount;
83
+
84
+ /// Evaluate one frame (pose-only). Pass `plane16` = nil to trigger
85
+ /// the C++ angular-delta fallback; otherwise pass a 16-element NSArray
86
+ /// of NSNumber (NSDoubles or NSFloats) holding the plane transform
87
+ /// column-major (matching `simd_float4x4` element order).
88
+ ///
89
+ /// Backward-compat entry point — always runs the C++ Pose strategy
90
+ /// regardless of `strategy` setting (since Flow needs the frame).
91
+ /// New code should call `evaluatePixelBuffer:…` below.
92
+ - (KGBDecision *)evaluateWithTx:(float)tx
93
+ ty:(float)ty
94
+ tz:(float)tz
95
+ qx:(float)qx
96
+ qy:(float)qy
97
+ qz:(float)qz
98
+ qw:(float)qw
99
+ fx:(float)fx
100
+ fy:(float)fy
101
+ cx:(float)cx
102
+ cy:(float)cy
103
+ imageWidth:(int32_t)imageWidth
104
+ imageHeight:(int32_t)imageHeight
105
+ plane16:(nullable NSArray<NSNumber *> *)plane16;
106
+
107
+ /// V16 A2 — strategy-aware evaluate that also accepts the frame's
108
+ /// pixel buffer. Required by Flow strategy (sparse-optical-flow
109
+ /// novelty needs the image content). Pose strategy ignores the
110
+ /// pixel buffer here — same result + cost as `evaluateWith…plane16:`.
111
+ ///
112
+ /// Supported pixel formats:
113
+ /// * `kCVPixelFormatType_420YpCbCr8BiPlanar{FullRange,VideoRange}`
114
+ /// — ARKit's native format. Y plane is read directly as
115
+ /// grayscale (no conversion cost).
116
+ /// * `kCVPixelFormatType_32BGRA` — converted to grayscale via
117
+ /// `cv::cvtColor` (~2-3 ms at 1920×1440).
118
+ /// Other formats → falls through to Pose strategy (defensive).
119
+ ///
120
+ /// The buffer is locked for the duration of the call. Caller can
121
+ /// safely release/recycle the buffer after this method returns.
122
+ - (KGBDecision *)evaluatePixelBuffer:(CVPixelBufferRef)pixelBuffer
123
+ tx:(float)tx
124
+ ty:(float)ty
125
+ tz:(float)tz
126
+ qx:(float)qx
127
+ qy:(float)qy
128
+ qz:(float)qz
129
+ qw:(float)qw
130
+ fx:(float)fx
131
+ fy:(float)fy
132
+ cx:(float)cx
133
+ cy:(float)cy
134
+ imageWidth:(int32_t)imageWidth
135
+ imageHeight:(int32_t)imageHeight
136
+ plane16:(nullable NSArray<NSNumber *> *)plane16
137
+ NS_SWIFT_NAME(evaluate(pixelBuffer:tx:ty:tz:qx:qy:qz:qw:fx:fy:cx:cy:imageWidth:imageHeight:plane16:));
138
+
139
+ @end
140
+
141
+ NS_ASSUME_NONNULL_END