react-native-image-stitcher 0.15.2 → 0.16.1
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 +171 -1
- package/README.md +131 -5
- package/android/src/main/cpp/image_stitcher_jni.cpp +154 -13
- package/android/src/main/cpp/keyframe_gate_jni.cpp +15 -8
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +223 -1
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +220 -87
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +1 -1
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +7 -36
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +14 -8
- package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +39 -1
- package/cpp/crop_quad.cpp +162 -0
- package/cpp/crop_quad.hpp +163 -0
- package/cpp/keyframe_gate.cpp +54 -15
- package/cpp/keyframe_gate.hpp +33 -0
- package/cpp/stitcher.cpp +1122 -132
- package/cpp/stitcher.hpp +62 -0
- package/cpp/warp_guard.hpp +212 -0
- package/dist/camera/Camera.d.ts +209 -12
- package/dist/camera/Camera.js +575 -36
- package/dist/camera/CameraView.js +35 -16
- package/dist/camera/CaptureCountdownOverlay.d.ts +70 -0
- package/dist/camera/CaptureCountdownOverlay.js +239 -0
- package/dist/camera/CaptureFrameCounterOverlay.d.ts +58 -0
- package/dist/camera/CaptureFrameCounterOverlay.js +153 -0
- package/dist/camera/CaptureMemoryPill.d.ts +24 -8
- package/dist/camera/CaptureMemoryPill.js +37 -12
- package/dist/camera/CapturePreview.js +2 -1
- package/dist/camera/CaptureStatusOverlay.d.ts +11 -4
- package/dist/camera/CaptureStatusOverlay.js +22 -5
- package/dist/camera/CaptureThumbnailStrip.js +2 -1
- package/dist/camera/LateralMotionModal.d.ts +85 -0
- package/dist/camera/LateralMotionModal.js +134 -0
- package/dist/camera/PanHowToOverlay.d.ts +76 -0
- package/dist/camera/PanHowToOverlay.js +222 -0
- package/dist/camera/PanoramaBandOverlay.d.ts +2 -1
- package/dist/camera/PanoramaBandOverlay.js +9 -3
- package/dist/camera/PanoramaSettings.d.ts +8 -6
- package/dist/camera/PanoramaSettings.js +19 -1
- package/dist/camera/PanoramaSettingsModal.js +4 -4
- package/dist/camera/RectCropPreview.d.ts +135 -0
- package/dist/camera/RectCropPreview.js +370 -0
- package/dist/camera/RotateToLandscapePrompt.d.ts +87 -0
- package/dist/camera/RotateToLandscapePrompt.js +138 -0
- package/dist/camera/buildPanoramaInitialSettings.d.ts +19 -2
- package/dist/camera/buildPanoramaInitialSettings.js +9 -0
- package/dist/camera/cameraErrorMessages.d.ts +30 -1
- package/dist/camera/cameraErrorMessages.js +26 -10
- package/dist/camera/cameraGuidanceCopy.d.ts +87 -0
- package/dist/camera/cameraGuidanceCopy.js +80 -0
- package/dist/camera/captureCountdown.d.ts +52 -0
- package/dist/camera/captureCountdown.js +76 -0
- package/dist/camera/captureWarnings.d.ts +90 -0
- package/dist/camera/captureWarnings.js +108 -0
- package/dist/camera/classifyStitchError.d.ts +30 -0
- package/dist/camera/classifyStitchError.js +42 -0
- package/dist/camera/cropGeometry.d.ts +136 -0
- package/dist/camera/cropGeometry.js +223 -0
- package/dist/camera/displayDecodeImageProps.d.ts +25 -0
- package/dist/camera/displayDecodeImageProps.js +29 -0
- package/dist/camera/guidanceGraphics.d.ts +58 -0
- package/dist/camera/guidanceGraphics.js +280 -0
- package/dist/camera/guidanceTokens.d.ts +54 -0
- package/dist/camera/guidanceTokens.js +58 -0
- package/dist/camera/panModeGate.d.ts +54 -0
- package/dist/camera/panModeGate.js +62 -0
- package/dist/camera/pickCaptureFormat.d.ts +71 -0
- package/dist/camera/pickCaptureFormat.js +85 -0
- package/dist/camera/stitchDebugInfo.d.ts +27 -0
- package/dist/camera/stitchDebugInfo.js +55 -0
- package/dist/camera/usePanMotion.d.ts +250 -0
- package/dist/camera/usePanMotion.js +451 -0
- package/dist/index.d.ts +24 -3
- package/dist/index.js +33 -2
- package/dist/stitching/computeInscribedRect.d.ts +40 -0
- package/dist/stitching/computeInscribedRect.js +55 -0
- package/dist/stitching/cropQuad.d.ts +78 -0
- package/dist/stitching/cropQuad.js +116 -0
- package/dist/stitching/incremental.d.ts +74 -0
- package/dist/stitching/useIncrementalStitcher.d.ts +7 -1
- package/dist/stitching/useIncrementalStitcher.js +7 -1
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +154 -29
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +4 -0
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +15 -0
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +48 -5
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +27 -0
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +211 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +25 -1
- package/ios/Sources/RNImageStitcher/Stitcher.swift +34 -1
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +5 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +56 -0
- package/package.json +5 -1
- package/src/camera/Camera.tsx +945 -47
- package/src/camera/CameraView.tsx +48 -16
- package/src/camera/CaptureCountdownOverlay.tsx +272 -0
- package/src/camera/CaptureFrameCounterOverlay.tsx +197 -0
- package/src/camera/CaptureMemoryPill.tsx +50 -12
- package/src/camera/CapturePreview.tsx +5 -0
- package/src/camera/CaptureStatusOverlay.tsx +35 -7
- package/src/camera/CaptureThumbnailStrip.tsx +4 -0
- package/src/camera/LateralMotionModal.tsx +199 -0
- package/src/camera/PanHowToOverlay.tsx +246 -0
- package/src/camera/PanoramaBandOverlay.tsx +9 -1
- package/src/camera/PanoramaSettings.ts +27 -7
- package/src/camera/PanoramaSettingsModal.tsx +4 -4
- package/src/camera/RectCropPreview.tsx +638 -0
- package/src/camera/RotateToLandscapePrompt.tsx +188 -0
- package/src/camera/buildPanoramaInitialSettings.ts +30 -1
- package/src/camera/cameraErrorMessages.ts +39 -2
- package/src/camera/cameraGuidanceCopy.ts +145 -0
- package/src/camera/captureCountdown.ts +83 -0
- package/src/camera/captureWarnings.ts +190 -0
- package/src/camera/classifyStitchError.ts +68 -0
- package/src/camera/cropGeometry.ts +268 -0
- package/src/camera/displayDecodeImageProps.ts +25 -0
- package/src/camera/guidanceGraphics.tsx +347 -0
- package/src/camera/guidanceTokens.ts +57 -0
- package/src/camera/panModeGate.ts +81 -0
- package/src/camera/pickCaptureFormat.ts +130 -0
- package/src/camera/stitchDebugInfo.ts +71 -0
- package/src/camera/usePanMotion.ts +667 -0
- package/src/index.ts +66 -3
- package/src/stitching/computeInscribedRect.ts +81 -0
- package/src/stitching/cropQuad.ts +167 -0
- package/src/stitching/incremental.ts +74 -0
- package/src/stitching/useIncrementalStitcher.ts +13 -0
- package/android/src/main/java/io/imagestitcher/rn/TransferredNV21.kt +0 -100
- package/cpp/tests/CMakeLists.txt +0 -104
- package/cpp/tests/README.md +0 -86
- package/cpp/tests/keyframe_timebudget_test.cpp +0 -65
- package/cpp/tests/pose_test.cpp +0 -74
- package/cpp/tests/stitcher_frame_data_test.cpp +0 -132
- package/cpp/tests/stubs/jsi/jsi.h +0 -33
- package/cpp/tests/stubs/react-native-worklets-core/WKTJsiWorklet.h +0 -34
- package/cpp/tests/warp_guard_test.cpp +0 -48
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +0 -190
- package/src/camera/__tests__/bandThumbRotation.test.ts +0 -120
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +0 -160
- package/src/camera/__tests__/cameraErrorMessages.test.ts +0 -76
- package/src/camera/__tests__/homeIndicatorEdge.test.ts +0 -116
- package/src/camera/__tests__/lowMemDevice.test.ts +0 -52
- package/src/camera/__tests__/selectCaptureDevice.test.ts +0 -210
- package/src/camera/__tests__/useContentRotation.test.ts +0 -89
- package/src/camera/__tests__/useOrientationDrift.test.ts +0 -169
- package/src/stitching/__tests__/subscribeIncrementalState.refine.test.ts +0 -276
- package/src/stitching/__tests__/useStitcherWorklet.test.ts +0 -202
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// crop_quad.cpp — OpenCV implementation of the free-quad perspective
|
|
4
|
+
// crop (`cropToQuad`), the only net-new native code for item-7 of the
|
|
5
|
+
// first-time-user guidance flow.
|
|
6
|
+
//
|
|
7
|
+
// Pairs with the EXISTING axis-aligned crop (cropToRectAtPath on iOS /
|
|
8
|
+
// cropToRect on Android). Where that crop slices a sub-rectangle out of
|
|
9
|
+
// the panorama, this one takes 4 user-dragged corners (a skewed
|
|
10
|
+
// quadrilateral) and perspective-rectifies them into an upright rectangle
|
|
11
|
+
// — the editor in src/camera/RectCropPreview.tsx picks `cropToQuad` over
|
|
12
|
+
// `cropToRect` whenever the dragged quad isn't ~axis-aligned.
|
|
13
|
+
//
|
|
14
|
+
// Why a separate .cpp (not folded into stitcher.cpp): the geometry core
|
|
15
|
+
// (quadDstRect / isQuadAcceptable) is the OpenCV-FREE, unit-tested
|
|
16
|
+
// cpp/crop_quad.hpp; this file is JUST the thin cv:: warp around it, kept
|
|
17
|
+
// out of stitcher.cpp's translation unit so the test suite can link the
|
|
18
|
+
// header without pulling in the whole stitch pipeline.
|
|
19
|
+
//
|
|
20
|
+
// Both platform bridges (iOS OpenCVStitcher.mm, Android BatchStitcher.kt)
|
|
21
|
+
// duplicate this warp in their own native language today — see the
|
|
22
|
+
// integrator notes in the item-7 handoff — so this file is the shared
|
|
23
|
+
// C++ reference + the home of the canvasExceedsGuard wiring. Wire it
|
|
24
|
+
// into a translation unit (the pod / the JNI lib) when the native crop is
|
|
25
|
+
// routed through shared C++ rather than per-platform OpenCV bindings.
|
|
26
|
+
|
|
27
|
+
// OpenCV's headers redefine NO/YES on platforms whose prefix.pch already
|
|
28
|
+
// has the ObjC bool macros; undef defensively (no-op off iOS).
|
|
29
|
+
#ifdef NO
|
|
30
|
+
#undef NO
|
|
31
|
+
#endif
|
|
32
|
+
#ifdef YES
|
|
33
|
+
#undef YES
|
|
34
|
+
#endif
|
|
35
|
+
|
|
36
|
+
#include <opencv2/opencv.hpp>
|
|
37
|
+
#include <opencv2/imgproc.hpp>
|
|
38
|
+
|
|
39
|
+
#include <string>
|
|
40
|
+
#include <vector>
|
|
41
|
+
|
|
42
|
+
#include "crop_quad.hpp"
|
|
43
|
+
#include "warp_guard.hpp"
|
|
44
|
+
|
|
45
|
+
namespace retailens {
|
|
46
|
+
|
|
47
|
+
// Result of a cropToQuad call. `ok == false` carries a human-readable
|
|
48
|
+
// `error` (the bridge maps it to NSError / Promise.reject); on success
|
|
49
|
+
// `width`/`height` are the written output dimensions.
|
|
50
|
+
struct CropQuadResult {
|
|
51
|
+
bool ok = false;
|
|
52
|
+
std::string error;
|
|
53
|
+
int width = 0;
|
|
54
|
+
int height = 0;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Perspective-rectify the quad `q` (4 ORDERED [TL, TR, BR, BL] image-pixel
|
|
58
|
+
// corners) out of the image at `inPath` into an upright w×h rectangle,
|
|
59
|
+
// written to `outPath` as a JPEG at `quality` (clamped to [1, 100]).
|
|
60
|
+
//
|
|
61
|
+
// Guards, in order (each is a hard reject — NO partial output is written):
|
|
62
|
+
// 1. inPath decodes (cv::imread non-empty).
|
|
63
|
+
// 2. isQuadAcceptable — convex + min-area + within the decoded image
|
|
64
|
+
// bounds. A degenerate / non-convex / out-of-bounds quad is rejected
|
|
65
|
+
// here, before any allocation.
|
|
66
|
+
// 3. quadDstRect yields a positive w×h.
|
|
67
|
+
// 4. canvasExceedsGuard(w, h) — the SAME blend-canvas guard the stitch
|
|
68
|
+
// pipeline uses. A near-collinear quad whose averaged opposite-edge
|
|
69
|
+
// lengths still multiply to a multi-gigapixel output (e.g. a 1 px ×
|
|
70
|
+
// 40000 px sliver dragged across a wide pano) can't OOM the device:
|
|
71
|
+
// the output Mat is never allocated when this fires.
|
|
72
|
+
//
|
|
73
|
+
// The warp itself is cv::getPerspectiveTransform(src → axis-aligned dst)
|
|
74
|
+
// + cv::warpPerspective to a w×h canvas. `outPath == inPath` is allowed
|
|
75
|
+
// (overwrite in place), matching cropToRect's contract.
|
|
76
|
+
CropQuadResult cropQuadToFile(const std::string& inPath,
|
|
77
|
+
const std::string& outPath,
|
|
78
|
+
const CropQuad& q,
|
|
79
|
+
int quality) {
|
|
80
|
+
CropQuadResult result;
|
|
81
|
+
|
|
82
|
+
cv::Mat img = cv::imread(inPath, cv::IMREAD_COLOR);
|
|
83
|
+
if (img.empty()) {
|
|
84
|
+
result.error = "Could not decode image at " + inPath;
|
|
85
|
+
return result;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Guard 2 — geometry: convex, non-degenerate, inside the decoded image.
|
|
89
|
+
if (!isQuadAcceptable(q, static_cast<double>(img.cols),
|
|
90
|
+
static_cast<double>(img.rows))) {
|
|
91
|
+
result.error =
|
|
92
|
+
"Crop quad is degenerate (non-convex, zero-area, or out of bounds)";
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Guard 3 — positive destination size.
|
|
97
|
+
const QuadDstSize dst = quadDstRect(q);
|
|
98
|
+
if (dst.width <= 0 || dst.height <= 0) {
|
|
99
|
+
result.error = "Crop quad rectifies to a non-positive rectangle";
|
|
100
|
+
return result;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Guard 4 — output-canvas OOM net (shared with the stitch pipeline).
|
|
104
|
+
if (canvasExceedsGuard(dst.width, dst.height)) {
|
|
105
|
+
result.error = "Crop quad output canvas exceeds the size guard (" +
|
|
106
|
+
std::to_string(dst.width) + "x" +
|
|
107
|
+
std::to_string(dst.height) + ")";
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const cv::Point2f src[4] = {
|
|
112
|
+
cv::Point2f(static_cast<float>(q.tl.x), static_cast<float>(q.tl.y)),
|
|
113
|
+
cv::Point2f(static_cast<float>(q.tr.x), static_cast<float>(q.tr.y)),
|
|
114
|
+
cv::Point2f(static_cast<float>(q.br.x), static_cast<float>(q.br.y)),
|
|
115
|
+
cv::Point2f(static_cast<float>(q.bl.x), static_cast<float>(q.bl.y)),
|
|
116
|
+
};
|
|
117
|
+
const cv::Point2f dstPts[4] = {
|
|
118
|
+
cv::Point2f(0.0f, 0.0f),
|
|
119
|
+
cv::Point2f(static_cast<float>(dst.width), 0.0f),
|
|
120
|
+
cv::Point2f(static_cast<float>(dst.width),
|
|
121
|
+
static_cast<float>(dst.height)),
|
|
122
|
+
cv::Point2f(0.0f, static_cast<float>(dst.height)),
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
cv::Mat warped;
|
|
126
|
+
try {
|
|
127
|
+
const cv::Mat transform = cv::getPerspectiveTransform(src, dstPts);
|
|
128
|
+
cv::warpPerspective(img, warped, transform,
|
|
129
|
+
cv::Size(dst.width, dst.height),
|
|
130
|
+
cv::INTER_LINEAR);
|
|
131
|
+
} catch (const cv::Exception& e) {
|
|
132
|
+
result.error = std::string("Perspective warp failed: ") + e.what();
|
|
133
|
+
return result;
|
|
134
|
+
}
|
|
135
|
+
if (warped.empty()) {
|
|
136
|
+
result.error = "Perspective warp produced an empty image";
|
|
137
|
+
return result;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
int q255 = quality;
|
|
141
|
+
if (q255 < 1) q255 = 1;
|
|
142
|
+
if (q255 > 100) q255 = 100;
|
|
143
|
+
const std::vector<int> writeParams = {cv::IMWRITE_JPEG_QUALITY, q255};
|
|
144
|
+
bool wrote = false;
|
|
145
|
+
try {
|
|
146
|
+
wrote = cv::imwrite(outPath, warped, writeParams);
|
|
147
|
+
} catch (const cv::Exception& e) {
|
|
148
|
+
result.error = std::string("Could not write cropped image: ") + e.what();
|
|
149
|
+
return result;
|
|
150
|
+
}
|
|
151
|
+
if (!wrote) {
|
|
152
|
+
result.error = "Could not write cropped image to " + outPath;
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
result.ok = true;
|
|
157
|
+
result.width = warped.cols;
|
|
158
|
+
result.height = warped.rows;
|
|
159
|
+
return result;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
} // namespace retailens
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
#pragma once
|
|
3
|
+
#include <cmath>
|
|
4
|
+
#include <cstdint>
|
|
5
|
+
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
7
|
+
// Free-quad crop geometry — the OpenCV-FREE, unit-testable core of the
|
|
8
|
+
// item-7 draggable-corner crop (`cropToQuad`). The actual perspective
|
|
9
|
+
// warp (cv::getPerspectiveTransform + cv::warpPerspective) lives next to
|
|
10
|
+
// the existing axis-aligned crop in the platform bridges; this header is
|
|
11
|
+
// JUST the two pure predicates worth testing in isolation:
|
|
12
|
+
//
|
|
13
|
+
// - quadDstRect(quad) → the {w,h} of the upright destination
|
|
14
|
+
// rectangle the quad rectifies INTO.
|
|
15
|
+
// - isQuadAcceptable(quad) → convex + min-area + within-bounds gate
|
|
16
|
+
// the warp must pass before allocating.
|
|
17
|
+
//
|
|
18
|
+
// Header-only + zero cv:: dependency on purpose — same posture as
|
|
19
|
+
// warp_guard.hpp. These are the C++ twins of the JS-side helpers in
|
|
20
|
+
// src/camera/cropGeometry.ts (rectSizeForQuad / isQuadValid); the math
|
|
21
|
+
// is duplicated per the repo's "duplicate stage code, DRY when proven"
|
|
22
|
+
// convention so a native caller never has to round-trip through JS to
|
|
23
|
+
// validate a quad.
|
|
24
|
+
//
|
|
25
|
+
// Corner-order contract: every function below assumes the 4 points are
|
|
26
|
+
// in canonical [TL, TR, BR, BL] (clockwise from top-left) winding — the
|
|
27
|
+
// order src/camera/cropGeometry.ts:orderQuadCorners produces and the
|
|
28
|
+
// order RectCropResult.quad carries. Pass un-ordered points and the
|
|
29
|
+
// edge-length / convexity math is meaningless.
|
|
30
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
namespace retailens {
|
|
33
|
+
|
|
34
|
+
// A single corner in image-pixel space (origin = image top-left).
|
|
35
|
+
struct QuadPoint {
|
|
36
|
+
double x = 0.0;
|
|
37
|
+
double y = 0.0;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Exactly four corners, in [TL, TR, BR, BL] order.
|
|
41
|
+
struct CropQuad {
|
|
42
|
+
QuadPoint tl;
|
|
43
|
+
QuadPoint tr;
|
|
44
|
+
QuadPoint br;
|
|
45
|
+
QuadPoint bl;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// Integer destination-rectangle size the quad rectifies into.
|
|
49
|
+
struct QuadDstSize {
|
|
50
|
+
int width = 0;
|
|
51
|
+
int height = 0;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Euclidean distance between two corners.
|
|
55
|
+
inline double quadPointDistance(const QuadPoint& a, const QuadPoint& b) {
|
|
56
|
+
const double dx = a.x - b.x;
|
|
57
|
+
const double dy = a.y - b.y;
|
|
58
|
+
return std::sqrt(dx * dx + dy * dy);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Target rectangle size for the perspective `dst` quad, derived from the
|
|
62
|
+
// 4 ORDERED ([TL, TR, BR, BL]) image-pixel corners:
|
|
63
|
+
// - width = average of the top edge (TL→TR) and bottom edge (BL→BR).
|
|
64
|
+
// - height = average of the left edge (TL→BL) and right edge (TR→BR).
|
|
65
|
+
//
|
|
66
|
+
// Averaging opposite edges gives a stable output size for a skewed quad
|
|
67
|
+
// (each pair of opposite edges differs under perspective; the mean is the
|
|
68
|
+
// least-distorting target). Rounds to whole pixels — the warp allocates
|
|
69
|
+
// an integer-sized output Mat. Mirrors cropGeometry.ts:rectSizeForQuad
|
|
70
|
+
// so iOS / Android / JS agree on the output dimensions bit-for-bit.
|
|
71
|
+
//
|
|
72
|
+
// A degenerate quad (all-collinear, zero-size) yields a 0×0 or 1×N size;
|
|
73
|
+
// the caller GUARDS this with isQuadAcceptable + canvasExceedsGuard before
|
|
74
|
+
// allocating, so this function never has to reject — it only measures.
|
|
75
|
+
inline QuadDstSize quadDstRect(const CropQuad& q) {
|
|
76
|
+
const double top = quadPointDistance(q.tl, q.tr);
|
|
77
|
+
const double bottom = quadPointDistance(q.bl, q.br);
|
|
78
|
+
const double left = quadPointDistance(q.tl, q.bl);
|
|
79
|
+
const double right = quadPointDistance(q.tr, q.br);
|
|
80
|
+
QuadDstSize size;
|
|
81
|
+
size.width = static_cast<int>(std::lround((top + bottom) / 2.0));
|
|
82
|
+
size.height = static_cast<int>(std::lround((left + right) / 2.0));
|
|
83
|
+
return size;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 2× the signed area of the quad via the shoelace formula. Positive for
|
|
87
|
+
// one winding, negative for the other, ~0 for a degenerate (collinear)
|
|
88
|
+
// quad. Sign is winding-dependent; callers that care about magnitude
|
|
89
|
+
// (min-area) take the absolute value.
|
|
90
|
+
inline double quadSignedArea2(const CropQuad& q) {
|
|
91
|
+
const QuadPoint p[4] = {q.tl, q.tr, q.br, q.bl};
|
|
92
|
+
double sum = 0.0;
|
|
93
|
+
for (int i = 0; i < 4; ++i) {
|
|
94
|
+
const QuadPoint& a = p[i];
|
|
95
|
+
const QuadPoint& b = p[(i + 1) % 4];
|
|
96
|
+
sum += a.x * b.y - b.x * a.y;
|
|
97
|
+
}
|
|
98
|
+
return sum;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Convexity test: all four consecutive edge cross-products share one sign
|
|
102
|
+
// (zero allowed for a straight, axis-aligned corner). Rejects the self-
|
|
103
|
+
// intersecting "bowtie" quads a free-drag editor can produce, which a
|
|
104
|
+
// perspective warp can't rectify. Winding-agnostic. Mirrors the
|
|
105
|
+
// `isConvex` helper in cropGeometry.ts.
|
|
106
|
+
inline bool quadIsConvex(const CropQuad& q) {
|
|
107
|
+
const QuadPoint p[4] = {q.tl, q.tr, q.br, q.bl};
|
|
108
|
+
int sign = 0;
|
|
109
|
+
for (int i = 0; i < 4; ++i) {
|
|
110
|
+
const QuadPoint& a = p[i];
|
|
111
|
+
const QuadPoint& b = p[(i + 1) % 4];
|
|
112
|
+
const QuadPoint& c = p[(i + 2) % 4];
|
|
113
|
+
const double cross =
|
|
114
|
+
(b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
|
|
115
|
+
if (cross != 0.0) {
|
|
116
|
+
const int s = (cross > 0.0) ? 1 : -1;
|
|
117
|
+
if (sign == 0) {
|
|
118
|
+
sign = s;
|
|
119
|
+
} else if (s != sign) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// True when the 4 ordered ([TL, TR, BR, BL]) corners form a quad the
|
|
128
|
+
// perspective warp can safely rectify:
|
|
129
|
+
// 1. **Convex** — no self-intersection (quadIsConvex).
|
|
130
|
+
// 2. **Non-degenerate area** — |signed area| ≥ `minArea` (default 1 px²);
|
|
131
|
+
// rejects all-collinear / zero-size quads.
|
|
132
|
+
// 3. **Within bounds** — every corner lies inside [0..imageW]×[0..imageH]
|
|
133
|
+
// (a half-pixel epsilon absorbs the lround in the JS letterbox
|
|
134
|
+
// inverse). Pass imageW <= 0 OR imageH <= 0 to SKIP the bounds
|
|
135
|
+
// check (the caller doesn't know the image size yet).
|
|
136
|
+
//
|
|
137
|
+
// The companion to warp_guard.hpp:canvasExceedsGuard — that guards the
|
|
138
|
+
// OUTPUT canvas against an OOM; this guards the INPUT quad against being
|
|
139
|
+
// geometrically unwarpable. Both must pass before warpPerspective runs.
|
|
140
|
+
inline bool isQuadAcceptable(const CropQuad& q,
|
|
141
|
+
double imageW = 0.0,
|
|
142
|
+
double imageH = 0.0,
|
|
143
|
+
double minArea = 1.0) {
|
|
144
|
+
if (!quadIsConvex(q)) {
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
if (std::fabs(quadSignedArea2(q)) < minArea * 2.0) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (imageW > 0.0 && imageH > 0.0) {
|
|
151
|
+
const QuadPoint p[4] = {q.tl, q.tr, q.br, q.bl};
|
|
152
|
+
const double eps = 0.5;
|
|
153
|
+
for (int i = 0; i < 4; ++i) {
|
|
154
|
+
if (p[i].x < -eps || p[i].x > imageW + eps ||
|
|
155
|
+
p[i].y < -eps || p[i].y > imageH + eps) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
} // namespace retailens
|
package/cpp/keyframe_gate.cpp
CHANGED
|
@@ -356,6 +356,11 @@ struct KeyframeGate::Impl {
|
|
|
356
356
|
// the downscale factor. Re-set whenever prevFrameGrayWork is.
|
|
357
357
|
int32_t prevFrameOrigWidth = 0;
|
|
358
358
|
int32_t prevFrameOrigHeight = 0;
|
|
359
|
+
// 2026-06-16 (audit #4) — the CURRENT frame's downscaled working image,
|
|
360
|
+
// produced by ingestWorkingFrame() (under the JNI pin) and consumed by
|
|
361
|
+
// evaluateWithWorkingMat() (after the pin is released). Empty unless an
|
|
362
|
+
// ingest is pending. Owned (downscaleToWorking clones / resizes).
|
|
363
|
+
cv::Mat pendingWorkMat;
|
|
359
364
|
};
|
|
360
365
|
|
|
361
366
|
// Compile-time layout check on the shared POD struct — ensures iOS
|
|
@@ -750,6 +755,45 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
|
|
|
750
755
|
int32_t height,
|
|
751
756
|
int32_t stride,
|
|
752
757
|
int64_t monotonicNowMs)
|
|
758
|
+
{
|
|
759
|
+
// 2026-06-16 (audit #4) — thin wrapper = ingest (reads pixels) + evaluate.
|
|
760
|
+
// Kept for iOS / non-pinned callers; the Android JNI calls the two halves
|
|
761
|
+
// separately so the heavy OpenCV runs OUTSIDE the GC-pinned critical region.
|
|
762
|
+
ingestWorkingFrame(grayData, width, height, stride);
|
|
763
|
+
return evaluateWithWorkingMat(pose, latchedPlane, width, height, monotonicNowMs);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
void KeyframeGate::ingestWorkingFrame(
|
|
767
|
+
const uint8_t* grayData,
|
|
768
|
+
int32_t width,
|
|
769
|
+
int32_t height,
|
|
770
|
+
int32_t stride)
|
|
771
|
+
{
|
|
772
|
+
Impl& s = *pImpl_;
|
|
773
|
+
s.pendingWorkMat.release();
|
|
774
|
+
// Only the Flow strategy reads pixels; disabled / force-accept / Pose ignore
|
|
775
|
+
// them (the Pose path is deliberately OpenCV-free). For those — and for
|
|
776
|
+
// invalid input — stash nothing; evaluateWithWorkingMat short-circuits
|
|
777
|
+
// without a frame. This is the ONLY step that touches `grayData`, so the
|
|
778
|
+
// JNI can ReleasePrimitiveArrayCritical immediately after it returns.
|
|
779
|
+
if (!s.enabled || s.forceAcceptNext || s.strategy != GateStrategy::Flow
|
|
780
|
+
|| grayData == nullptr || width <= 0 || height <= 0 || stride < width) {
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Non-owning view over the (pinned) bytes; downscaleToWorking returns an
|
|
784
|
+
// OWNED Mat (clone or resize output), so it stays valid after the pin drops.
|
|
785
|
+
cv::Mat currGrayFull(height, width, CV_8UC1,
|
|
786
|
+
const_cast<uint8_t*>(grayData),
|
|
787
|
+
static_cast<size_t>(stride));
|
|
788
|
+
s.pendingWorkMat = downscaleToWorking(currGrayFull);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
KeyframeGateDecision KeyframeGate::evaluateWithWorkingMat(
|
|
792
|
+
const Pose& pose,
|
|
793
|
+
const PlaneTransform* latchedPlane,
|
|
794
|
+
int32_t origWidth,
|
|
795
|
+
int32_t origHeight,
|
|
796
|
+
int64_t monotonicNowMs)
|
|
753
797
|
{
|
|
754
798
|
Impl& s = *pImpl_;
|
|
755
799
|
// Stash the monotonic stamp (see evaluate()). The Pose-strategy
|
|
@@ -785,19 +829,14 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
|
|
|
785
829
|
return evaluate(pose, latchedPlane, s.currentNowMs);
|
|
786
830
|
}
|
|
787
831
|
|
|
788
|
-
// Flow path —
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
// gracefully rather than crashing on a null deref.
|
|
832
|
+
// Flow path — operate on the working frame ingestWorkingFrame() stashed.
|
|
833
|
+
// Empty ⇒ ingest declined (null/invalid grayData under Flow); fall back to
|
|
834
|
+
// the pose path so we degrade gracefully rather than deref a null frame.
|
|
835
|
+
cv::Mat currGrayWork = std::move(s.pendingWorkMat);
|
|
836
|
+
s.pendingWorkMat.release();
|
|
837
|
+
if (currGrayWork.empty()) {
|
|
795
838
|
return evaluate(pose, latchedPlane, s.currentNowMs);
|
|
796
839
|
}
|
|
797
|
-
cv::Mat currGrayFull(height, width, CV_8UC1,
|
|
798
|
-
const_cast<uint8_t*>(grayData),
|
|
799
|
-
static_cast<size_t>(stride));
|
|
800
|
-
cv::Mat currGrayWork = downscaleToWorking(currGrayFull);
|
|
801
840
|
|
|
802
841
|
// §4 — first-frame accept under Flow. No prev to track against;
|
|
803
842
|
// we anchor here and detect features so subsequent frames have
|
|
@@ -811,8 +850,8 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
|
|
|
811
850
|
s.flowMinDistance);
|
|
812
851
|
s.prevFrameGrayWork = currGrayWork; // clone-owned via downscale path
|
|
813
852
|
s.prevFeatures = std::move(features);
|
|
814
|
-
s.prevFrameOrigWidth =
|
|
815
|
-
s.prevFrameOrigHeight =
|
|
853
|
+
s.prevFrameOrigWidth = origWidth;
|
|
854
|
+
s.prevFrameOrigHeight = origHeight;
|
|
816
855
|
s.lastAcceptedPose = pose;
|
|
817
856
|
s.lastAcceptSteadyMs = s.currentNowMs;
|
|
818
857
|
s.acceptedCount = 1;
|
|
@@ -970,8 +1009,8 @@ KeyframeGateDecision KeyframeGate::evaluateWithFrame(
|
|
|
970
1009
|
s.flowMinDistance);
|
|
971
1010
|
s.prevFrameGrayWork = currGrayWork; // owned via downscale's clone
|
|
972
1011
|
s.prevFeatures = std::move(nextFeatures);
|
|
973
|
-
s.prevFrameOrigWidth =
|
|
974
|
-
s.prevFrameOrigHeight =
|
|
1012
|
+
s.prevFrameOrigWidth = origWidth;
|
|
1013
|
+
s.prevFrameOrigHeight = origHeight;
|
|
975
1014
|
s.lastAcceptedPose = pose;
|
|
976
1015
|
s.lastAcceptSteadyMs = s.currentNowMs;
|
|
977
1016
|
s.acceptedCount += 1;
|
package/cpp/keyframe_gate.hpp
CHANGED
|
@@ -248,6 +248,39 @@ public:
|
|
|
248
248
|
int32_t stride,
|
|
249
249
|
int64_t monotonicNowMs = -1);
|
|
250
250
|
|
|
251
|
+
// ── Split entry point (2026-06-16, audit #4) ──────────────────
|
|
252
|
+
//
|
|
253
|
+
// Android JNI pins the gray byte[] with GetPrimitiveArrayCritical, which
|
|
254
|
+
// blocks the GC for the pin's whole duration. Running the heavy OpenCV
|
|
255
|
+
// (goodFeaturesToTrack / optical flow, multi-ms) inside that window stalls
|
|
256
|
+
// the frame-rate producer thread and violates the JNI no-blocking-between-
|
|
257
|
+
// critical rule. This pair splits the work so ONLY the pinned-buffer read
|
|
258
|
+
// happens under the pin:
|
|
259
|
+
//
|
|
260
|
+
// ingestWorkingFrame(grayData, w, h, stride) // <- under the pin
|
|
261
|
+
// Builds the downscaled working frame (the only step that reads the
|
|
262
|
+
// pinned pixels) and stashes it INSIDE the gate. Does nothing (no
|
|
263
|
+
// pixel read) for Pose/disabled/force-accept/invalid input — those
|
|
264
|
+
// don't need the frame. cv::Mat is kept out of this header so the
|
|
265
|
+
// JNI needn't link OpenCV; the frame lives on the gate's Impl.
|
|
266
|
+
//
|
|
267
|
+
// evaluateWithWorkingMat(pose, plane, origW, origH) // <- pin released
|
|
268
|
+
// Runs the rest of the gate (the heavy OpenCV) on the stashed working
|
|
269
|
+
// frame, OUTSIDE the critical section. origW/origH are the ORIGINAL
|
|
270
|
+
// full-res dims (the working frame is downscaled).
|
|
271
|
+
//
|
|
272
|
+
// evaluateWithFrame() above is exactly ingest+evaluate in sequence — kept
|
|
273
|
+
// for iOS and any non-pinned caller (DRY: it delegates to these two).
|
|
274
|
+
void ingestWorkingFrame(const uint8_t* grayData,
|
|
275
|
+
int32_t width,
|
|
276
|
+
int32_t height,
|
|
277
|
+
int32_t stride);
|
|
278
|
+
KeyframeGateDecision evaluateWithWorkingMat(const Pose& pose,
|
|
279
|
+
const PlaneTransform* latchedPlane,
|
|
280
|
+
int32_t origWidth,
|
|
281
|
+
int32_t origHeight,
|
|
282
|
+
int64_t monotonicNowMs = -1);
|
|
283
|
+
|
|
251
284
|
// ── State accessors (read-only, post-evaluate) ────────────────
|
|
252
285
|
int32_t getAcceptedCount() const;
|
|
253
286
|
int32_t getMaxCount() const;
|