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,278 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ //
3
+ // KeyframeGateBridge.mm — Obj-C++ glue between Swift and the shared
4
+ // C++ KeyframeGate. See header for design rationale.
5
+
6
+ #import "KeyframeGateBridge.h"
7
+ #import "keyframe_gate.hpp" // ../../../cpp/keyframe_gate.hpp via header search path
8
+ #import "ar_frame_pose.h"
9
+
10
+ // V16 A2 — OpenCV needed for the BGRA → grayscale conversion fallback
11
+ // in the new pixel-buffer evaluate path. ARKit's native YUV format
12
+ // (the dominant case) skips OpenCV entirely — Y plane is read
13
+ // directly as the grayscale source. We only pay the cvtColor cost
14
+ // for non-YUV buffers (e.g., vision-camera BGRA capture flows).
15
+ #import <opencv2/core.hpp>
16
+ #import <opencv2/imgproc.hpp>
17
+
18
+ // Single source of truth for the reason-code → string mapping. These
19
+ // strings MUST stay 1:1 with the labels emitted by the original
20
+ // KeyframeGate.swift (and read by the JS telemetry layer in
21
+ // retailens-capture-sdk/src/stitching/incremental.ts). Drift will
22
+ // silently break the JS UI's pill text.
23
+ static NSString *kReasonStringFor(retailens::KeyframeGateDecisionReason r) {
24
+ using R = retailens::KeyframeGateDecisionReason;
25
+ switch (r) {
26
+ case R::AcceptDisabled: return @"gate-disabled";
27
+ case R::AcceptForceLast: return @"force-last";
28
+ case R::AcceptFirstOnPlane: return @"first-anchored-on-plane";
29
+ case R::AcceptFirstNoPlane: return @"first-no-plane";
30
+ case R::AcceptOk: return @"ok";
31
+ case R::AcceptOkAngular: return @"ok-angular";
32
+ case R::AcceptProjectionDegenerate: return @"projection-degenerate";
33
+ case R::AcceptCurrentAreaZero: return @"current-area-zero";
34
+ case R::AcceptNoPoseYet: return @"no-pose-yet";
35
+ case R::RejectMaxReached: return @"max-reached";
36
+ case R::RejectOverlapTooHigh: return @"overlap-too-high";
37
+ case R::RejectOverlapTooHighAngular: return @"overlap-too-high (angular)";
38
+ // V16 A2 — flow strategy reason codes
39
+ case R::AcceptOkFlow: return @"ok-flow";
40
+ case R::AcceptFirstFlow: return @"first-flow";
41
+ case R::RejectOverlapTooHighFlow: return @"overlap-too-high (flow)";
42
+ // V16 — translation-budget force-accept
43
+ case R::AcceptFlowTranslation: return @"ok-flow-translation";
44
+ }
45
+ return @"unknown";
46
+ }
47
+
48
+ // ── KGBDecision impl ─────────────────────────────────────────────
49
+
50
+ @interface KGBDecision ()
51
+ @property (nonatomic, readwrite) BOOL accept;
52
+ @property (nonatomic, readwrite) NSInteger reasonCode;
53
+ @property (nonatomic, readwrite) NSString *reasonString;
54
+ @property (nonatomic, readwrite) double newContentFraction;
55
+ @property (nonatomic, readwrite) NSInteger acceptedCount;
56
+ @property (nonatomic, readwrite) NSInteger maxCount;
57
+ @end
58
+
59
+ @implementation KGBDecision
60
+ @end
61
+
62
+ // ── KeyframeGateBridge impl ──────────────────────────────────────
63
+
64
+ @implementation KeyframeGateBridge {
65
+ retailens::KeyframeGate _gate;
66
+ }
67
+
68
+ - (instancetype)init {
69
+ self = [super init];
70
+ // No init needed — KeyframeGate's default state is exactly right
71
+ // (enabled=false, threshold=0.4, maxCount=6, acceptedCount=0).
72
+ return self;
73
+ }
74
+
75
+ - (void)setEnabled:(BOOL)enabled {
76
+ _gate.setEnabled(static_cast<bool>(enabled));
77
+ }
78
+
79
+ - (void)setOverlapThreshold:(double)threshold {
80
+ _gate.setOverlapThreshold(threshold);
81
+ }
82
+
83
+ - (void)setMaxCount:(NSInteger)maxCount {
84
+ _gate.setMaxCount(static_cast<int32_t>(maxCount));
85
+ }
86
+
87
+ - (void)markNextFrameAsLast {
88
+ _gate.markNextFrameAsLast();
89
+ }
90
+
91
+ - (void)reset {
92
+ _gate.reset();
93
+ }
94
+
95
+ - (BOOL)isEnabled { return static_cast<BOOL>(_gate.isEnabled()); }
96
+ - (NSInteger)acceptedCount { return static_cast<NSInteger>(_gate.getAcceptedCount()); }
97
+ - (NSInteger)maxCount { return static_cast<NSInteger>(_gate.getMaxCount()); }
98
+
99
+ // ── V16 A2 — strategy + flow tunables ───────────────────────────
100
+
101
+ - (void)setStrategy:(KGBStrategy)strategy {
102
+ _gate.setStrategy(static_cast<retailens::GateStrategy>(strategy));
103
+ }
104
+
105
+ - (KGBStrategy)strategy {
106
+ return static_cast<KGBStrategy>(_gate.getStrategy());
107
+ }
108
+
109
+ - (void)setFlowMaxCorners:(NSInteger)maxCorners {
110
+ _gate.setFlowMaxCorners(static_cast<int32_t>(maxCorners));
111
+ }
112
+
113
+ - (void)setFlowQualityLevel:(double)quality {
114
+ _gate.setFlowQualityLevel(quality);
115
+ }
116
+
117
+ - (void)setFlowMinDistance:(double)minDistance {
118
+ _gate.setFlowMinDistance(minDistance);
119
+ }
120
+
121
+ - (void)setFlowMaxTranslationM:(double)metres {
122
+ _gate.setFlowMaxTranslationM(metres);
123
+ }
124
+
125
+ - (void)setFlowNoveltyPercentile:(double)percentile {
126
+ _gate.setFlowNoveltyPercentile(percentile);
127
+ }
128
+
129
+ - (KGBDecision *)evaluateWithTx:(float)tx ty:(float)ty tz:(float)tz
130
+ qx:(float)qx qy:(float)qy qz:(float)qz qw:(float)qw
131
+ fx:(float)fx fy:(float)fy cx:(float)cx cy:(float)cy
132
+ imageWidth:(int32_t)imageWidth
133
+ imageHeight:(int32_t)imageHeight
134
+ plane16:(nullable NSArray<NSNumber *> *)plane16
135
+ {
136
+ retailens::Pose pose;
137
+ pose.tx = tx; pose.ty = ty; pose.tz = tz;
138
+ pose.qx = qx; pose.qy = qy; pose.qz = qz; pose.qw = qw;
139
+ pose.fx = fx; pose.fy = fy; pose.cx = cx; pose.cy = cy;
140
+ pose.imageWidth = imageWidth;
141
+ pose.imageHeight = imageHeight;
142
+
143
+ retailens::PlaneTransform planeStorage;
144
+ const retailens::PlaneTransform *planePtr = nullptr;
145
+ if (plane16 != nil && plane16.count == 16) {
146
+ for (NSUInteger i = 0; i < 16; ++i) {
147
+ planeStorage.m[i] = static_cast<float>(plane16[i].doubleValue);
148
+ }
149
+ planePtr = &planeStorage;
150
+ }
151
+
152
+ retailens::KeyframeGateDecision d = _gate.evaluate(pose, planePtr);
153
+
154
+ KGBDecision *out = [[KGBDecision alloc] init];
155
+ out.accept = static_cast<BOOL>(d.accept);
156
+ out.reasonCode = static_cast<NSInteger>(d.reason);
157
+ out.reasonString = kReasonStringFor(d.reason);
158
+ out.newContentFraction = d.newContentFraction;
159
+ out.acceptedCount = static_cast<NSInteger>(d.acceptedCount);
160
+ out.maxCount = static_cast<NSInteger>(d.maxCount);
161
+ return out;
162
+ }
163
+
164
+ // V16 A2 — strategy-aware evaluate that also accepts the frame's
165
+ // pixel buffer. See header for format support + cost notes.
166
+ //
167
+ // Internal flow:
168
+ // 1. Build the retailens::Pose + optional plane (same as
169
+ // evaluateWith…plane16:).
170
+ // 2. Lock the pixel buffer (read-only — we never write back).
171
+ // 3. Get a grayscale view of the frame: Y-plane direct read for
172
+ // YUV 4:2:0, cv::cvtColor for BGRA. Other formats → fall
173
+ // through to the pose-only path (defensive).
174
+ // 4. Hand grayscale data pointer + dims + stride to the C++ gate
175
+ // via evaluateWithFrame(...).
176
+ // 5. Unlock the pixel buffer.
177
+ // 6. Marshal the decision back to KGBDecision.
178
+ //
179
+ // The grayscale data MUST stay alive for the duration of the C++
180
+ // call: for YUV that's guaranteed by the lock; for BGRA we hold
181
+ // the cvtColor result in a stack-local cv::Mat (`bgraToGrayHolder`)
182
+ // whose buffer outlives the gate call.
183
+ - (KGBDecision *)evaluatePixelBuffer:(CVPixelBufferRef)pixelBuffer
184
+ tx:(float)tx ty:(float)ty tz:(float)tz
185
+ qx:(float)qx qy:(float)qy qz:(float)qz qw:(float)qw
186
+ fx:(float)fx fy:(float)fy cx:(float)cx cy:(float)cy
187
+ imageWidth:(int32_t)imageWidth
188
+ imageHeight:(int32_t)imageHeight
189
+ plane16:(nullable NSArray<NSNumber *> *)plane16
190
+ {
191
+ // Fast path: Pose strategy doesn't need the pixel buffer; route
192
+ // straight to the pose-only evaluate so we don't pay the
193
+ // CVPixelBuffer lock/unlock cost (~10 µs but at 60 fps = 600 µs/s
194
+ // wasted) for every Pose-strategy frame.
195
+ if (_gate.getStrategy() == retailens::GateStrategy::Pose) {
196
+ return [self evaluateWithTx:tx ty:ty tz:tz
197
+ qx:qx qy:qy qz:qz qw:qw
198
+ fx:fx fy:fy cx:cx cy:cy
199
+ imageWidth:imageWidth
200
+ imageHeight:imageHeight
201
+ plane16:plane16];
202
+ }
203
+
204
+ retailens::Pose pose;
205
+ pose.tx = tx; pose.ty = ty; pose.tz = tz;
206
+ pose.qx = qx; pose.qy = qy; pose.qz = qz; pose.qw = qw;
207
+ pose.fx = fx; pose.fy = fy; pose.cx = cx; pose.cy = cy;
208
+ pose.imageWidth = imageWidth;
209
+ pose.imageHeight = imageHeight;
210
+
211
+ retailens::PlaneTransform planeStorage;
212
+ const retailens::PlaneTransform *planePtr = nullptr;
213
+ if (plane16 != nil && plane16.count == 16) {
214
+ for (NSUInteger i = 0; i < 16; ++i) {
215
+ planeStorage.m[i] = static_cast<float>(plane16[i].doubleValue);
216
+ }
217
+ planePtr = &planeStorage;
218
+ }
219
+
220
+ // Lock the buffer for read-only access. Caller (the engine's AR
221
+ // delegate) might want to use the same buffer for its own
222
+ // processing — the lock is reentrant within the same thread, so
223
+ // this is safe.
224
+ CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
225
+
226
+ const OSType format = CVPixelBufferGetPixelFormatType(pixelBuffer);
227
+ const uint8_t *grayData = nullptr;
228
+ int32_t grayWidth = 0;
229
+ int32_t grayHeight = 0;
230
+ int32_t grayStride = 0;
231
+ cv::Mat bgraToGrayHolder; // owns the converted buffer for BGRA path
232
+
233
+ if (format == kCVPixelFormatType_420YpCbCr8BiPlanarFullRange ||
234
+ format == kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange)
235
+ {
236
+ // YUV 4:2:0 biplanar — Y plane IS our grayscale. Zero
237
+ // conversion cost. ARKit's native format; the dominant
238
+ // production path.
239
+ grayData = static_cast<const uint8_t *>(
240
+ CVPixelBufferGetBaseAddressOfPlane(pixelBuffer, 0));
241
+ grayWidth = static_cast<int32_t>(CVPixelBufferGetWidthOfPlane(pixelBuffer, 0));
242
+ grayHeight = static_cast<int32_t>(CVPixelBufferGetHeightOfPlane(pixelBuffer, 0));
243
+ grayStride = static_cast<int32_t>(CVPixelBufferGetBytesPerRowOfPlane(pixelBuffer, 0));
244
+ } else if (format == kCVPixelFormatType_32BGRA) {
245
+ // BGRA — convert to grayscale via OpenCV. ~2-3 ms at
246
+ // 1920×1440 on iPhone 13 Pro.
247
+ const int32_t w = static_cast<int32_t>(CVPixelBufferGetWidth(pixelBuffer));
248
+ const int32_t h = static_cast<int32_t>(CVPixelBufferGetHeight(pixelBuffer));
249
+ const int32_t bgraStride = static_cast<int32_t>(CVPixelBufferGetBytesPerRow(pixelBuffer));
250
+ uint8_t *bgraPtr = static_cast<uint8_t *>(CVPixelBufferGetBaseAddress(pixelBuffer));
251
+ cv::Mat bgra(h, w, CV_8UC4, bgraPtr, bgraStride);
252
+ cv::cvtColor(bgra, bgraToGrayHolder, cv::COLOR_BGRA2GRAY);
253
+ grayData = bgraToGrayHolder.data;
254
+ grayWidth = static_cast<int32_t>(bgraToGrayHolder.cols);
255
+ grayHeight = static_cast<int32_t>(bgraToGrayHolder.rows);
256
+ grayStride = static_cast<int32_t>(bgraToGrayHolder.step);
257
+ }
258
+ // else: grayData stays nullptr. The C++ gate detects this and
259
+ // falls back to the pose-only path inside evaluateWithFrame —
260
+ // graceful degradation for unsupported pixel formats.
261
+
262
+ retailens::KeyframeGateDecision d = _gate.evaluateWithFrame(
263
+ pose, planePtr,
264
+ grayData, grayWidth, grayHeight, grayStride);
265
+
266
+ CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
267
+
268
+ KGBDecision *out = [[KGBDecision alloc] init];
269
+ out.accept = static_cast<BOOL>(d.accept);
270
+ out.reasonCode = static_cast<NSInteger>(d.reason);
271
+ out.reasonString = kReasonStringFor(d.reason);
272
+ out.newContentFraction = d.newContentFraction;
273
+ out.acceptedCount = static_cast<NSInteger>(d.acceptedCount);
274
+ out.maxCount = static_cast<NSInteger>(d.maxCount);
275
+ return out;
276
+ }
277
+
278
+ @end