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.
- package/CHANGELOG.md +96 -0
- package/LICENSE +201 -0
- package/NOTICE +21 -0
- package/README.md +189 -0
- package/RNImageStitcher.podspec +76 -0
- package/android/build.gradle +224 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/cpp/CMakeLists.txt +124 -0
- package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
- package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
- package/cpp/ar_frame_pose.h +63 -0
- package/cpp/keyframe_gate.cpp +927 -0
- package/cpp/keyframe_gate.hpp +240 -0
- package/cpp/stitcher.cpp +2207 -0
- package/cpp/stitcher.hpp +275 -0
- package/dist/ar/useARSession.d.ts +102 -0
- package/dist/ar/useARSession.js +133 -0
- package/dist/camera/ARCameraView.d.ts +93 -0
- package/dist/camera/ARCameraView.js +170 -0
- package/dist/camera/Camera.d.ts +134 -0
- package/dist/camera/Camera.js +688 -0
- package/dist/camera/CameraShutter.d.ts +80 -0
- package/dist/camera/CameraShutter.js +237 -0
- package/dist/camera/CameraView.d.ts +65 -0
- package/dist/camera/CameraView.js +117 -0
- package/dist/camera/CaptureControlsBar.d.ts +87 -0
- package/dist/camera/CaptureControlsBar.js +82 -0
- package/dist/camera/CaptureHeader.d.ts +62 -0
- package/dist/camera/CaptureHeader.js +81 -0
- package/dist/camera/CapturePreview.d.ts +70 -0
- package/dist/camera/CapturePreview.js +188 -0
- package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
- package/dist/camera/CaptureStatusOverlay.js +326 -0
- package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
- package/dist/camera/CaptureThumbnailStrip.js +177 -0
- package/dist/camera/IncrementalPanGuide.d.ts +83 -0
- package/dist/camera/IncrementalPanGuide.js +267 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
- package/dist/camera/PanoramaBandOverlay.js +399 -0
- package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
- package/dist/camera/PanoramaConfirmModal.js +128 -0
- package/dist/camera/PanoramaGuidance.d.ts +79 -0
- package/dist/camera/PanoramaGuidance.js +246 -0
- package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
- package/dist/camera/PanoramaSettingsModal.js +611 -0
- package/dist/camera/ViewportCropOverlay.d.ts +46 -0
- package/dist/camera/ViewportCropOverlay.js +67 -0
- package/dist/camera/useCapture.d.ts +111 -0
- package/dist/camera/useCapture.js +160 -0
- package/dist/camera/useDeviceOrientation.d.ts +48 -0
- package/dist/camera/useDeviceOrientation.js +131 -0
- package/dist/camera/useVideoCapture.d.ts +79 -0
- package/dist/camera/useVideoCapture.js +151 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +39 -0
- package/dist/quality/normaliseOrientation.d.ts +36 -0
- package/dist/quality/normaliseOrientation.js +62 -0
- package/dist/quality/runQualityCheck.d.ts +41 -0
- package/dist/quality/runQualityCheck.js +98 -0
- package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
- package/dist/sensors/useIMUTranslationGate.js +235 -0
- package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
- package/dist/stitching/IncrementalStitcherView.js +157 -0
- package/dist/stitching/incremental.d.ts +930 -0
- package/dist/stitching/incremental.js +133 -0
- package/dist/stitching/stitchFrames.d.ts +55 -0
- package/dist/stitching/stitchFrames.js +56 -0
- package/dist/stitching/stitchVideo.d.ts +119 -0
- package/dist/stitching/stitchVideo.js +57 -0
- package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
- package/dist/stitching/useIncrementalJSDriver.js +199 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
- package/dist/stitching/useIncrementalStitcher.js +172 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +15 -0
- package/ios/Package.swift +72 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
- package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
- package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
- package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
- package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
- package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
- package/package.json +73 -0
- package/react-native.config.js +34 -0
- package/scripts/opencv-version.txt +1 -0
- package/scripts/postinstall-fetch-binaries.js +286 -0
- package/src/ar/useARSession.ts +210 -0
- package/src/camera/.gitkeep +0 -0
- package/src/camera/ARCameraView.tsx +256 -0
- package/src/camera/Camera.tsx +1053 -0
- package/src/camera/CameraShutter.tsx +292 -0
- package/src/camera/CameraView.tsx +157 -0
- package/src/camera/CaptureControlsBar.tsx +204 -0
- package/src/camera/CaptureHeader.tsx +184 -0
- package/src/camera/CapturePreview.tsx +318 -0
- package/src/camera/CaptureStatusOverlay.tsx +391 -0
- package/src/camera/CaptureThumbnailStrip.tsx +277 -0
- package/src/camera/IncrementalPanGuide.tsx +328 -0
- package/src/camera/PanoramaBandOverlay.tsx +498 -0
- package/src/camera/PanoramaConfirmModal.tsx +206 -0
- package/src/camera/PanoramaGuidance.tsx +327 -0
- package/src/camera/PanoramaSettingsModal.tsx +1357 -0
- package/src/camera/ViewportCropOverlay.tsx +81 -0
- package/src/camera/useCapture.ts +279 -0
- package/src/camera/useDeviceOrientation.ts +140 -0
- package/src/camera/useVideoCapture.ts +236 -0
- package/src/index.ts +53 -0
- package/src/quality/.gitkeep +0 -0
- package/src/quality/normaliseOrientation.ts +79 -0
- package/src/quality/runQualityCheck.ts +131 -0
- package/src/sensors/useIMUTranslationGate.ts +347 -0
- package/src/stitching/.gitkeep +0 -0
- package/src/stitching/IncrementalStitcherView.tsx +198 -0
- package/src/stitching/incremental.ts +1021 -0
- package/src/stitching/stitchFrames.ts +88 -0
- package/src/stitching/stitchVideo.ts +153 -0
- package/src/stitching/useIncrementalJSDriver.ts +273 -0
- package/src/stitching/useIncrementalStitcher.ts +252 -0
- 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
|