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
package/cpp/stitcher.hpp
ADDED
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
//
|
|
3
|
+
// stitcher.hpp — shared cv::Stitcher orchestration used by both
|
|
4
|
+
// iOS (via Obj-C++ bridge in OpenCVStitcherBridge.mm) and Android
|
|
5
|
+
// (via JNI in image_stitcher_jni.cpp).
|
|
6
|
+
//
|
|
7
|
+
// Why this exists
|
|
8
|
+
// ───────────────
|
|
9
|
+
//
|
|
10
|
+
// Before 2026-05-15, iOS had a hand-rolled cv::detail::* pipeline
|
|
11
|
+
// (~3,000 lines in OpenCVStitcher.mm) while Android used the
|
|
12
|
+
// high-level cv::Stitcher::create() API (~600 lines in
|
|
13
|
+
// image_stitcher_jni.cpp). Two implementations of the same algorithm
|
|
14
|
+
// drifted independently — fixes landed on one platform and didn't on
|
|
15
|
+
// the other. This file collapses that into a single source of truth.
|
|
16
|
+
//
|
|
17
|
+
// V1 scope (this commit): port the Android high-level pipeline
|
|
18
|
+
// verbatim into shared C++ + add the C+D progressive-confidence retry
|
|
19
|
+
// loop + dimension/memory instrumentation. Both platforms call this.
|
|
20
|
+
//
|
|
21
|
+
// V2 scope (follow-up): port iOS's manual cv::detail::* pipeline
|
|
22
|
+
// features (explicit leaveBiggestComponent retry around
|
|
23
|
+
// just the prune step rather than the whole stitch — 5-10× cheaper;
|
|
24
|
+
// wave correction; exposure compensator) into this file as a
|
|
25
|
+
// SECOND code path selectable via StitchConfig::useManualPipeline.
|
|
26
|
+
// Until then, iOS gets the Android-level capability set.
|
|
27
|
+
//
|
|
28
|
+
// API design
|
|
29
|
+
// ──────────
|
|
30
|
+
//
|
|
31
|
+
// One function: stitchFramePaths(framePaths, outputPath, config).
|
|
32
|
+
//
|
|
33
|
+
// All inputs marshalled as primitive C++ types. Output is a
|
|
34
|
+
// StitchResult struct that carries success/error info + the C+D
|
|
35
|
+
// drop-count telemetry. Loggin happens via an optional callback so
|
|
36
|
+
// the iOS bridge can plumb to os_log and the Android bridge to
|
|
37
|
+
// __android_log_print — same source, different sink.
|
|
38
|
+
//
|
|
39
|
+
// Threading: not thread-safe. Caller must serialise. Each
|
|
40
|
+
// invocation is independent (no shared mutable state).
|
|
41
|
+
|
|
42
|
+
#pragma once
|
|
43
|
+
|
|
44
|
+
#include <cstdint>
|
|
45
|
+
#include <functional>
|
|
46
|
+
#include <string>
|
|
47
|
+
#include <vector>
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
namespace retailens {
|
|
51
|
+
|
|
52
|
+
// Stable error codes. Mirror the JS-side `StitchErrorCode` enum so
|
|
53
|
+
// the bridge layers can map these to NSError.code / Java throwable
|
|
54
|
+
// without translation tables.
|
|
55
|
+
//
|
|
56
|
+
// Bit-for-bit aligned to cv::Stitcher::Status values where possible
|
|
57
|
+
// (NeedMoreImages, HomographyEstimationFailed, CameraParamsAdjustFailed)
|
|
58
|
+
// + a few new codes for failure modes that cv::Stitcher itself
|
|
59
|
+
// doesn't surface (image read/write failure, all-frames-dropped).
|
|
60
|
+
//
|
|
61
|
+
// Manual-pipeline-specific failure modes (added 2026-05-15 as part of
|
|
62
|
+
// the V2 shared-port code-review pass; previously these all collapsed
|
|
63
|
+
// into UnknownCvException, which made post-mortem triage from JS-side
|
|
64
|
+
// telemetry impossible):
|
|
65
|
+
// PreStitchMemoryAbort — manual pipeline detected RSS above
|
|
66
|
+
// the per-device pre-stitch threshold
|
|
67
|
+
// and bailed before allocating compose
|
|
68
|
+
// buffers. Operator should retry on
|
|
69
|
+
// fresh app launch or lower compose MP.
|
|
70
|
+
// ComposeResizeFailed — cv::resize threw inside the compose-
|
|
71
|
+
// stage downscale loop (Step 7c). Most
|
|
72
|
+
// commonly a recycled-mmap allocator
|
|
73
|
+
// issue when stitching consecutively;
|
|
74
|
+
// a fresh process usually recovers.
|
|
75
|
+
// WarpFailed — warper->warp threw inside the warp
|
|
76
|
+
// loop (Step 8b). Camera params from
|
|
77
|
+
// BA may be degenerate; check
|
|
78
|
+
// framesIncluded vs framesRequested.
|
|
79
|
+
// EmptyPanorama — blender->blend completed but produced
|
|
80
|
+
// a 0×0 panorama. Should never happen
|
|
81
|
+
// in practice; if it does, the failure
|
|
82
|
+
// is upstream in the warp/feed loop.
|
|
83
|
+
enum class StitchErrorCode : int32_t {
|
|
84
|
+
Ok = 0,
|
|
85
|
+
NeedMoreImages = 1, // cv::Stitcher::ERR_NEED_MORE_IMGS
|
|
86
|
+
HomographyEstimationFailed = 2, // cv::Stitcher::ERR_HOMOGRAPHY_EST_FAIL
|
|
87
|
+
CameraParamsAdjustFailed = 3, // cv::Stitcher::ERR_CAMERA_PARAMS_ADJUST_FAIL
|
|
88
|
+
ImageReadFailed = 100,
|
|
89
|
+
ImageWriteFailed = 101,
|
|
90
|
+
AllFramesDroppedByConfidence = 102,
|
|
91
|
+
PreStitchMemoryAbort = 103,
|
|
92
|
+
ComposeResizeFailed = 104,
|
|
93
|
+
WarpFailed = 105,
|
|
94
|
+
EmptyPanorama = 106,
|
|
95
|
+
InvalidArgument = 200,
|
|
96
|
+
UnknownCvException = 300,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
// Stitcher mode selector — maps to cv::Stitcher::Mode.
|
|
101
|
+
//
|
|
102
|
+
// Panorama: rotation-only (spherical/cylindrical/plane warper +
|
|
103
|
+
// BundleAdjusterRay + BestOf2NearestMatcher). Best for
|
|
104
|
+
// rotate-in-place captures.
|
|
105
|
+
// Scans: affine (plane warper + BundleAdjusterAffine +
|
|
106
|
+
// AffineBestOf2NearestMatcher). Best for shelf-pan
|
|
107
|
+
// translation captures.
|
|
108
|
+
//
|
|
109
|
+
// Caller (typically the JS engineMode resolver) picks per capture.
|
|
110
|
+
// "auto" resolution happens UPSTREAM in JS via accumulated
|
|
111
|
+
// translation vs rotation totals from the KeyframeGate — by the
|
|
112
|
+
// time we get here, it's a concrete mode.
|
|
113
|
+
enum class StitchMode : int32_t {
|
|
114
|
+
Panorama = 0,
|
|
115
|
+
Scans = 1,
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
// Configuration bundle for a single stitch invocation. All fields
|
|
120
|
+
// have safe defaults so callers only override what they care about.
|
|
121
|
+
//
|
|
122
|
+
// Resolution budgets (`*ResolMP`) are in megapixels per frame:
|
|
123
|
+
// < 0.0 → entry-point picks its own appropriate default
|
|
124
|
+
// ≥ 0.0 → cap at this MP target via cv::Stitcher::set*Resol()
|
|
125
|
+
//
|
|
126
|
+
// compositingResolMP intentionally defaults to a NEGATIVE SENTINEL so
|
|
127
|
+
// the two entry points can pick different appropriate defaults:
|
|
128
|
+
//
|
|
129
|
+
// * High-level stitchFramePaths() (cv::Stitcher::create wrapper):
|
|
130
|
+
// falls back to 1.0 MP. cv::Stitcher's library default for
|
|
131
|
+
// compositing is ORIG_RESOL (-1.0) which composes at full sensor
|
|
132
|
+
// resolution and trivially OOMs on Android — 1.0 MP caps that
|
|
133
|
+
// while preserving most of the sharpness.
|
|
134
|
+
//
|
|
135
|
+
// * Manual stitchFramePathsManual() (cv::detail::* pipeline):
|
|
136
|
+
// falls back to 0.6 MP. The hand-rolled pipeline blends at
|
|
137
|
+
// compose-MP DIRECTLY (rather than re-warping from features-
|
|
138
|
+
// resolution work frames as cv::Stitcher does internally), which
|
|
139
|
+
// means memory peak scales more aggressively with compose-MP.
|
|
140
|
+
// 1.0 MP pushed iOS into jetsam territory; 0.6 MP is the "safe
|
|
141
|
+
// sharp" sweet spot documented in OpenCVStitcher.mm comments.
|
|
142
|
+
struct StitchConfig {
|
|
143
|
+
std::string warperType = "plane"; // "plane"|"cylindrical"|"spherical"
|
|
144
|
+
std::string blenderType = "multiband"; // "multiband"|"feather"
|
|
145
|
+
std::string seamFinderType = "graphcut"; // "graphcut"|"skip"|"voronoi"
|
|
146
|
+
StitchMode stitchMode = StitchMode::Panorama;
|
|
147
|
+
std::string captureOrientation = "portrait"; // "portrait"|"portrait-upside-down"|"landscape-left"|"landscape-right"
|
|
148
|
+
bool useInscribedRectCrop = false; // bbox-only crop is the default
|
|
149
|
+
double registrationResolMP = -1.0; // < 0 = cv default (0.6 MP)
|
|
150
|
+
double seamEstimationResolMP = -1.0; // < 0 = cv default (0.1 MP)
|
|
151
|
+
double compositingResolMP = -1.0; // < 0 = entry-specific default (high-level: 1.0 MP, manual: 0.6 MP)
|
|
152
|
+
int jpegQuality = 85;
|
|
153
|
+
|
|
154
|
+
// Total device RAM in megabytes. Used by the manual pipeline's
|
|
155
|
+
// pre-stitch memory abort heuristic to decide whether to short-
|
|
156
|
+
// circuit a stitch that would likely OOM. When < 0 (default),
|
|
157
|
+
// falls back to a conservative assumption (4 GB = kAssumedTotalRAMGB
|
|
158
|
+
// in stitcher.cpp). Callers should plumb:
|
|
159
|
+
// iOS: NSProcessInfo.processInfo.physicalMemory / (1024*1024)
|
|
160
|
+
// Android: ActivityManager.getMemoryInfo().totalMem / (1024*1024)
|
|
161
|
+
// or sysconf(_SC_PHYS_PAGES) * sysconf(_SC_PAGE_SIZE) / (1024*1024)
|
|
162
|
+
double availableRamMB = -1.0;
|
|
163
|
+
|
|
164
|
+
// Manual-pipeline opt-in (V2 of the shared port).
|
|
165
|
+
//
|
|
166
|
+
// Set true to route stitchFramePaths() through the hand-rolled
|
|
167
|
+
// cv::detail::* pipeline implemented in stitchFramePathsManual()
|
|
168
|
+
// instead of the high-level cv::Stitcher::create() wrapper. The
|
|
169
|
+
// manual pipeline gives finer control that the high-level API
|
|
170
|
+
// hides behind defaults that don't fit our shelf-pan capture
|
|
171
|
+
// shape:
|
|
172
|
+
//
|
|
173
|
+
// * Seam finder runs at a SEPARATE seam-MP budget (~0.1 MP
|
|
174
|
+
// default) and the seam mask is upscaled back to compose-MP
|
|
175
|
+
// before feeding the blender. GraphCut is roughly O(N²) in
|
|
176
|
+
// pixels — running it at compose-MP (1.0 MP) costs ~100× more
|
|
177
|
+
// than at seam-MP and was timing out finalize() in JS.
|
|
178
|
+
//
|
|
179
|
+
// * MultiBandBlender's Laplacian pyramid is built at compose-MP
|
|
180
|
+
// directly (rather than re-warping from features-resolution
|
|
181
|
+
// work frames). Cylindrical-era sharpness restored on iOS.
|
|
182
|
+
//
|
|
183
|
+
// * leaveBiggestComponent runs at PRUNE granularity (i.e., the
|
|
184
|
+
// retry happens BEFORE the expensive BA / warp / blend), not
|
|
185
|
+
// around the whole pipeline. Retry cost is 5-10× cheaper than
|
|
186
|
+
// the high-level cv::Stitcher's C+D loop that re-runs every
|
|
187
|
+
// stage at each threshold.
|
|
188
|
+
//
|
|
189
|
+
// * Explicit BundleAdjusterRay + wave correction + median focal
|
|
190
|
+
// length scale determination — all features cv::Stitcher does
|
|
191
|
+
// internally but with parameters we can't override (iter cap,
|
|
192
|
+
// wave-correct kind, confidence threshold).
|
|
193
|
+
//
|
|
194
|
+
// Android currently leaves this false (the high-level pipeline
|
|
195
|
+
// works fine on Android's pre-V16 keyframe budgets). iOS will
|
|
196
|
+
// flip it to true once the manual port is verified — separate
|
|
197
|
+
// commit from this V2 introduction.
|
|
198
|
+
bool useManualPipeline = false;
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
// Result returned to the caller. On success: outputPath written +
|
|
203
|
+
// dimensions + C+D telemetry. On failure: errorCode + errorMessage
|
|
204
|
+
// (errorCode is the primary signal; message is human-readable).
|
|
205
|
+
struct StitchResult {
|
|
206
|
+
bool success = false;
|
|
207
|
+
StitchErrorCode errorCode = StitchErrorCode::UnknownCvException;
|
|
208
|
+
std::string errorMessage;
|
|
209
|
+
|
|
210
|
+
int32_t width = 0;
|
|
211
|
+
int32_t height = 0;
|
|
212
|
+
|
|
213
|
+
// C+D telemetry — filled in even on success. See
|
|
214
|
+
// 2026-05-15 commit 57ecccd for context.
|
|
215
|
+
int32_t framesRequested = 0;
|
|
216
|
+
int32_t framesIncluded = 0;
|
|
217
|
+
double finalConfidenceThresh = -1.0; // The threshold value that succeeded; -1 if not relevant.
|
|
218
|
+
|
|
219
|
+
int64_t durationMs = 0;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
// Logging callback type — bridge layers plug their platform logger
|
|
224
|
+
// (os_log on iOS, __android_log_print on Android). Use nullptr to
|
|
225
|
+
// silence.
|
|
226
|
+
//
|
|
227
|
+
// level: 0=info, 1=warn, 2=error
|
|
228
|
+
// tag: short tag like "[stitch]" or "[dimstat]"
|
|
229
|
+
// msg: the formatted message (caller must format before passing)
|
|
230
|
+
using LogFn = std::function<void(int level, const char* tag, const char* msg)>;
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
// Primary entry point. Loads input JPEGs, configures cv::Stitcher
|
|
234
|
+
// per the config, runs the C+D progressive-confidence retry loop,
|
|
235
|
+
// crops, bake-rotates, writes the output JPEG.
|
|
236
|
+
//
|
|
237
|
+
// When `config.useManualPipeline` is true the call is routed to
|
|
238
|
+
// `stitchFramePathsManual()` instead — see below for the manual
|
|
239
|
+
// pipeline's structural differences.
|
|
240
|
+
//
|
|
241
|
+
// Thread-safe per-call (no shared state); caller must serialise
|
|
242
|
+
// concurrent calls.
|
|
243
|
+
//
|
|
244
|
+
// On failure (StitchResult::success == false) the output file is
|
|
245
|
+
// not written. errorCode + errorMessage tell why.
|
|
246
|
+
StitchResult stitchFramePaths(
|
|
247
|
+
const std::vector<std::string>& framePaths,
|
|
248
|
+
const std::string& outputPath,
|
|
249
|
+
const StitchConfig& config,
|
|
250
|
+
LogFn logFn = nullptr);
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
// Manual cv::detail::* pipeline entry point. Same input/output
|
|
254
|
+
// contract as stitchFramePaths(), but uses the hand-rolled stitching
|
|
255
|
+
// pipeline ported from OpenCVStitcher.mm (ORB → BestOf2NearestMatcher
|
|
256
|
+
// → HomographyBasedEstimator → BundleAdjusterRay → wave correct →
|
|
257
|
+
// median-focal warper scale → two-stage resolution (registration_MP
|
|
258
|
+
// / compose_MP) → GraphCutSeamFinder at seam_MP → MultiBandBlender).
|
|
259
|
+
//
|
|
260
|
+
// Use via config.useManualPipeline = true to get this entry point
|
|
261
|
+
// indirectly from stitchFramePaths(). Also callable directly if a
|
|
262
|
+
// future caller wants to bypass the high-level wrapper entirely.
|
|
263
|
+
//
|
|
264
|
+
// Thread-safe per-call (no shared state); caller must serialise
|
|
265
|
+
// concurrent calls.
|
|
266
|
+
//
|
|
267
|
+
// On failure (StitchResult::success == false) the output file is
|
|
268
|
+
// not written. errorCode + errorMessage tell why.
|
|
269
|
+
StitchResult stitchFramePathsManual(
|
|
270
|
+
const std::vector<std::string>& framePaths,
|
|
271
|
+
const std::string& outputPath,
|
|
272
|
+
const StitchConfig& config,
|
|
273
|
+
LogFn logFn = nullptr);
|
|
274
|
+
|
|
275
|
+
} // namespace retailens
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useARSession — React hook for the SDK's ARKit (iOS) / ARCore
|
|
3
|
+
* (Android) session foundation.
|
|
4
|
+
*
|
|
5
|
+
* Phase 4 of the AR measurement plan
|
|
6
|
+
* (docs/site-content/design/2026-04-29-ar-measurement-and-detection.md).
|
|
7
|
+
*
|
|
8
|
+
* What this gives the host:
|
|
9
|
+
* - `isAvailable`: whether the device can run AR at all
|
|
10
|
+
* - `trackingState`: current AR tracking quality (mirrors Apple's
|
|
11
|
+
* enum values exactly — same numeric ids on both platforms)
|
|
12
|
+
* - `start()` / `stop()`: lifecycle controls
|
|
13
|
+
* - `getFramePoses()`: snapshot the per-frame pose log captured
|
|
14
|
+
* during the most recent session, used by Phase 5 stitching
|
|
15
|
+
* and Phase 6 measurement
|
|
16
|
+
*
|
|
17
|
+
* What this does NOT give:
|
|
18
|
+
* The hook is camera-agnostic. It just runs the AR tracking
|
|
19
|
+
* session. Frame display + capture happen via the SDK's
|
|
20
|
+
* AR-backed `<CameraView>` (Phase 4.4 — coming) or the existing
|
|
21
|
+
* vision-camera-backed view if AR is unavailable.
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* AR tracking state. Numeric values mirror iOS' enum and the
|
|
25
|
+
* Android constants in `RNSARSession.companion`. Cross-
|
|
26
|
+
* platform identical; no branching needed in JS.
|
|
27
|
+
*/
|
|
28
|
+
export declare enum ARTrackingState {
|
|
29
|
+
/** AR not running, not supported, or session was stopped. */
|
|
30
|
+
NotAvailable = 0,
|
|
31
|
+
/** Session running but tracking quality not yet usable. */
|
|
32
|
+
Initialising = 1,
|
|
33
|
+
/** Session running with usable tracking — poses are good. */
|
|
34
|
+
Tracking = 2,
|
|
35
|
+
/** Tracking quality dropped mid-session. Poses degraded. */
|
|
36
|
+
Limited = 3
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* One captured frame's pose. Coordinates are in the AR session's
|
|
40
|
+
* world frame (right-handed, Y-up on iOS / Y-up on Android), with
|
|
41
|
+
* translation in metres. Rotation is a unit quaternion; w is the
|
|
42
|
+
* real component.
|
|
43
|
+
*/
|
|
44
|
+
export interface FramePose {
|
|
45
|
+
tx: number;
|
|
46
|
+
ty: number;
|
|
47
|
+
tz: number;
|
|
48
|
+
qx: number;
|
|
49
|
+
qy: number;
|
|
50
|
+
qz: number;
|
|
51
|
+
qw: number;
|
|
52
|
+
/** Camera intrinsics (focal length + principal point) in pixels. */
|
|
53
|
+
fx: number;
|
|
54
|
+
fy: number;
|
|
55
|
+
cx: number;
|
|
56
|
+
cy: number;
|
|
57
|
+
imageWidth: number;
|
|
58
|
+
imageHeight: number;
|
|
59
|
+
/** Frame timestamp in ms relative to AR session start. */
|
|
60
|
+
timestampMs: number;
|
|
61
|
+
trackingState: ARTrackingState;
|
|
62
|
+
}
|
|
63
|
+
export interface UseARSessionReturn {
|
|
64
|
+
/**
|
|
65
|
+
* Whether the device can run AR. Set after the first `start()`
|
|
66
|
+
* call (or by the explicit `checkAvailability()`). False on
|
|
67
|
+
* older iPhones, simulators, and unsupported Android devices.
|
|
68
|
+
*/
|
|
69
|
+
isAvailable: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Whether the session is currently running. True between
|
|
72
|
+
* `start()` and `stop()`.
|
|
73
|
+
*/
|
|
74
|
+
isRunning: boolean;
|
|
75
|
+
/** Current tracking quality. Polled every 500ms while running. */
|
|
76
|
+
trackingState: ARTrackingState;
|
|
77
|
+
/**
|
|
78
|
+
* Start the AR session. On Android, may trigger a Play Services
|
|
79
|
+
* for AR install dialog the first time it runs. Throws if the
|
|
80
|
+
* device doesn't support AR.
|
|
81
|
+
*/
|
|
82
|
+
start: () => Promise<void>;
|
|
83
|
+
/**
|
|
84
|
+
* Stop the AR session and clear the pose log. Idempotent;
|
|
85
|
+
* calling on a stopped session is a no-op.
|
|
86
|
+
*/
|
|
87
|
+
stop: () => Promise<void>;
|
|
88
|
+
/**
|
|
89
|
+
* Snapshot the per-frame pose log captured since the last
|
|
90
|
+
* `start()`. Used by the stitcher and measurement APIs after
|
|
91
|
+
* recording stops.
|
|
92
|
+
*/
|
|
93
|
+
getFramePoses: () => Promise<FramePose[]>;
|
|
94
|
+
/**
|
|
95
|
+
* Drop everything in the pose log. Call before each new
|
|
96
|
+
* panorama capture so the log doesn't carry stale poses from
|
|
97
|
+
* an earlier session.
|
|
98
|
+
*/
|
|
99
|
+
clearPoseLog: () => Promise<void>;
|
|
100
|
+
}
|
|
101
|
+
export declare function useARSession(): UseARSessionReturn;
|
|
102
|
+
//# sourceMappingURL=useARSession.d.ts.map
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* useARSession — React hook for the SDK's ARKit (iOS) / ARCore
|
|
5
|
+
* (Android) session foundation.
|
|
6
|
+
*
|
|
7
|
+
* Phase 4 of the AR measurement plan
|
|
8
|
+
* (docs/site-content/design/2026-04-29-ar-measurement-and-detection.md).
|
|
9
|
+
*
|
|
10
|
+
* What this gives the host:
|
|
11
|
+
* - `isAvailable`: whether the device can run AR at all
|
|
12
|
+
* - `trackingState`: current AR tracking quality (mirrors Apple's
|
|
13
|
+
* enum values exactly — same numeric ids on both platforms)
|
|
14
|
+
* - `start()` / `stop()`: lifecycle controls
|
|
15
|
+
* - `getFramePoses()`: snapshot the per-frame pose log captured
|
|
16
|
+
* during the most recent session, used by Phase 5 stitching
|
|
17
|
+
* and Phase 6 measurement
|
|
18
|
+
*
|
|
19
|
+
* What this does NOT give:
|
|
20
|
+
* The hook is camera-agnostic. It just runs the AR tracking
|
|
21
|
+
* session. Frame display + capture happen via the SDK's
|
|
22
|
+
* AR-backed `<CameraView>` (Phase 4.4 — coming) or the existing
|
|
23
|
+
* vision-camera-backed view if AR is unavailable.
|
|
24
|
+
*/
|
|
25
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
26
|
+
exports.ARTrackingState = void 0;
|
|
27
|
+
exports.useARSession = useARSession;
|
|
28
|
+
const react_1 = require("react");
|
|
29
|
+
const react_native_1 = require("react-native");
|
|
30
|
+
/**
|
|
31
|
+
* AR tracking state. Numeric values mirror iOS' enum and the
|
|
32
|
+
* Android constants in `RNSARSession.companion`. Cross-
|
|
33
|
+
* platform identical; no branching needed in JS.
|
|
34
|
+
*/
|
|
35
|
+
var ARTrackingState;
|
|
36
|
+
(function (ARTrackingState) {
|
|
37
|
+
/** AR not running, not supported, or session was stopped. */
|
|
38
|
+
ARTrackingState[ARTrackingState["NotAvailable"] = 0] = "NotAvailable";
|
|
39
|
+
/** Session running but tracking quality not yet usable. */
|
|
40
|
+
ARTrackingState[ARTrackingState["Initialising"] = 1] = "Initialising";
|
|
41
|
+
/** Session running with usable tracking — poses are good. */
|
|
42
|
+
ARTrackingState[ARTrackingState["Tracking"] = 2] = "Tracking";
|
|
43
|
+
/** Tracking quality dropped mid-session. Poses degraded. */
|
|
44
|
+
ARTrackingState[ARTrackingState["Limited"] = 3] = "Limited";
|
|
45
|
+
})(ARTrackingState || (exports.ARTrackingState = ARTrackingState = {}));
|
|
46
|
+
function getNativeModule() {
|
|
47
|
+
const m = react_native_1.NativeModules['RNSARSession'];
|
|
48
|
+
if (!m || typeof m !== 'object')
|
|
49
|
+
return null;
|
|
50
|
+
return m;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Polling interval for tracking-state updates. 500ms is enough to
|
|
54
|
+
* feel responsive in the UI without flooding the bridge.
|
|
55
|
+
*/
|
|
56
|
+
const STATE_POLL_INTERVAL_MS = 500;
|
|
57
|
+
function useARSession() {
|
|
58
|
+
const [isAvailable, setIsAvailable] = (0, react_1.useState)(false);
|
|
59
|
+
const [isRunning, setIsRunning] = (0, react_1.useState)(false);
|
|
60
|
+
const [trackingState, setTrackingState] = (0, react_1.useState)(ARTrackingState.NotAvailable);
|
|
61
|
+
const pollRef = (0, react_1.useRef)(null);
|
|
62
|
+
const native = getNativeModule();
|
|
63
|
+
// Probe availability once on mount. Running on a device without
|
|
64
|
+
// AR support shouldn't crash anything — `isAvailable` stays
|
|
65
|
+
// false and the rest of the SDK falls back to vision-camera.
|
|
66
|
+
(0, react_1.useEffect)(() => {
|
|
67
|
+
if (!native)
|
|
68
|
+
return;
|
|
69
|
+
native.isSupported().then(setIsAvailable).catch((err) => {
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn('[useARSession] isSupported failed', err);
|
|
72
|
+
});
|
|
73
|
+
}, [native]);
|
|
74
|
+
const stopPolling = (0, react_1.useCallback)(() => {
|
|
75
|
+
if (pollRef.current !== null) {
|
|
76
|
+
clearInterval(pollRef.current);
|
|
77
|
+
pollRef.current = null;
|
|
78
|
+
}
|
|
79
|
+
}, []);
|
|
80
|
+
const startPolling = (0, react_1.useCallback)(() => {
|
|
81
|
+
stopPolling();
|
|
82
|
+
if (!native)
|
|
83
|
+
return;
|
|
84
|
+
pollRef.current = setInterval(async () => {
|
|
85
|
+
try {
|
|
86
|
+
const state = await native.getState();
|
|
87
|
+
setIsRunning(state.isRunning);
|
|
88
|
+
setTrackingState(state.trackingState);
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// Bridge errors during tear-down are expected; ignore.
|
|
92
|
+
}
|
|
93
|
+
}, STATE_POLL_INTERVAL_MS);
|
|
94
|
+
}, [native, stopPolling]);
|
|
95
|
+
// Always tear down the poll on unmount.
|
|
96
|
+
(0, react_1.useEffect)(() => stopPolling, [stopPolling]);
|
|
97
|
+
const start = (0, react_1.useCallback)(async () => {
|
|
98
|
+
if (!native) {
|
|
99
|
+
throw new Error('useARSession: RNSARSession native module unavailable');
|
|
100
|
+
}
|
|
101
|
+
await native.start();
|
|
102
|
+
setIsRunning(true);
|
|
103
|
+
startPolling();
|
|
104
|
+
}, [native, startPolling]);
|
|
105
|
+
const stop = (0, react_1.useCallback)(async () => {
|
|
106
|
+
if (!native)
|
|
107
|
+
return;
|
|
108
|
+
stopPolling();
|
|
109
|
+
await native.stop();
|
|
110
|
+
setIsRunning(false);
|
|
111
|
+
setTrackingState(ARTrackingState.NotAvailable);
|
|
112
|
+
}, [native, stopPolling]);
|
|
113
|
+
const getFramePoses = (0, react_1.useCallback)(async () => {
|
|
114
|
+
if (!native)
|
|
115
|
+
return [];
|
|
116
|
+
return native.snapshotPoseLog();
|
|
117
|
+
}, [native]);
|
|
118
|
+
const clearPoseLog = (0, react_1.useCallback)(async () => {
|
|
119
|
+
if (!native)
|
|
120
|
+
return;
|
|
121
|
+
await native.clearPoseLog();
|
|
122
|
+
}, [native]);
|
|
123
|
+
return {
|
|
124
|
+
isAvailable,
|
|
125
|
+
isRunning,
|
|
126
|
+
trackingState,
|
|
127
|
+
start,
|
|
128
|
+
stop,
|
|
129
|
+
getFramePoses,
|
|
130
|
+
clearPoseLog,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=useARSession.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARCameraView — AR-backed alternative to ``<CameraView>`` for
|
|
3
|
+
* audits that need pose-aware capture (panorama mode, packet
|
|
4
|
+
* detection). Renders the ARKit camera feed via the native
|
|
5
|
+
* `RNSARCameraView` UIView; the underlying ARSession is the
|
|
6
|
+
* SDK singleton (`RNSARSession.shared`), shared between the
|
|
7
|
+
* preview and the pose log that feeds Phase 5 stitching + Phase 6
|
|
8
|
+
* measurement.
|
|
9
|
+
*
|
|
10
|
+
* Why a separate component (vs. a polymorphic CameraView)?
|
|
11
|
+
* 1. **Different imperative API.** The vision-camera-backed
|
|
12
|
+
* CameraView exposes `takePhoto / startRecording` via its ref
|
|
13
|
+
* (Phase 5 will add equivalents to this component, but they
|
|
14
|
+
* route through ARFrame.capturedImage + AVAssetWriter rather
|
|
15
|
+
* than vision-camera's APIs).
|
|
16
|
+
* 2. **Camera-access conflict.** ARKit and AVCaptureSession
|
|
17
|
+
* can't share the camera. Forcing the host to pick one
|
|
18
|
+
* component over the other (instead of toggling a prop on a
|
|
19
|
+
* shared component) makes the conflict impossible to misuse —
|
|
20
|
+
* you can't accidentally mount both at the same time.
|
|
21
|
+
* 3. **Lifecycle clarity.** The native side starts the AR
|
|
22
|
+
* session in `didMoveToWindow`. Mount = start, unmount =
|
|
23
|
+
* stop. No flag-twiddling.
|
|
24
|
+
*
|
|
25
|
+
* This component is preview-only in Phase 4.4. Photo + video
|
|
26
|
+
* capture come in Phase 5 (Step 5 of the AR design plan). Until
|
|
27
|
+
* then, the host's panorama capture flow continues to use
|
|
28
|
+
* vision-camera; ARCameraView is opt-in via a settings flag for
|
|
29
|
+
* developer verification.
|
|
30
|
+
*/
|
|
31
|
+
import React from 'react';
|
|
32
|
+
import { type ViewStyle } from 'react-native';
|
|
33
|
+
export interface ARCameraViewProps {
|
|
34
|
+
/** Layout style, typically `StyleSheet.absoluteFill` or `flex: 1`. */
|
|
35
|
+
style?: ViewStyle;
|
|
36
|
+
/**
|
|
37
|
+
* Optional themed guidance banner shown over the preview at the
|
|
38
|
+
* top, mirrors the `<CameraView>` prop so host apps can swap
|
|
39
|
+
* components without rewriting their guidance text plumbing.
|
|
40
|
+
*/
|
|
41
|
+
guidance?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Imperative handle exposed via the ref — shape mirrors the subset
|
|
45
|
+
* of vision-camera's `Camera` ref methods that the host's
|
|
46
|
+
* `useCapture` / `useVideoCapture` hooks call. Hosts can pass the
|
|
47
|
+
* SAME ref to those hooks as they do for the vision-camera path,
|
|
48
|
+
* with no branching required.
|
|
49
|
+
*
|
|
50
|
+
* Note we do NOT exhaustively mirror vision-camera's API surface —
|
|
51
|
+
* only the methods the panorama capture flow uses today. As the
|
|
52
|
+
* SDK grows AR-aware features, methods are added here.
|
|
53
|
+
*/
|
|
54
|
+
export interface ARCameraViewHandle {
|
|
55
|
+
/**
|
|
56
|
+
* Capture the latest ARFrame as a JPEG. Resolves with a
|
|
57
|
+
* vision-camera-compatible PhotoFile (`{ path, width, height,
|
|
58
|
+
* isMirrored, isRawPhoto }`). Native generates a temp path —
|
|
59
|
+
* caller does NOT need to construct one.
|
|
60
|
+
*/
|
|
61
|
+
takePhoto: (options?: {
|
|
62
|
+
quality?: number;
|
|
63
|
+
}) => Promise<{
|
|
64
|
+
path: string;
|
|
65
|
+
width: number;
|
|
66
|
+
height: number;
|
|
67
|
+
isMirrored: boolean;
|
|
68
|
+
isRawPhoto: boolean;
|
|
69
|
+
}>;
|
|
70
|
+
/**
|
|
71
|
+
* Begin recording AR frames into an mp4. Mirrors vision-camera's
|
|
72
|
+
* callback-based API: takes `onRecordingFinished` /
|
|
73
|
+
* `onRecordingError` handlers; the actual VideoFile is delivered
|
|
74
|
+
* via `onRecordingFinished` AFTER the host calls `stopRecording`.
|
|
75
|
+
*
|
|
76
|
+
* Synchronous return (void) — useVideoCapture wraps it in a
|
|
77
|
+
* Promise on top of the callbacks.
|
|
78
|
+
*/
|
|
79
|
+
startRecording: (options: {
|
|
80
|
+
onRecordingFinished?: (video: {
|
|
81
|
+
path: string;
|
|
82
|
+
duration: number;
|
|
83
|
+
size: number;
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
}) => void;
|
|
87
|
+
onRecordingError?: (err: Error) => void;
|
|
88
|
+
}) => void;
|
|
89
|
+
/** Finalise the in-progress recording. */
|
|
90
|
+
stopRecording: () => Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
export declare const ARCameraView: React.ForwardRefExoticComponent<ARCameraViewProps & React.RefAttributes<ARCameraViewHandle>>;
|
|
93
|
+
//# sourceMappingURL=ARCameraView.d.ts.map
|