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,133 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Incremental panorama-stitching native module bindings.
|
|
5
|
+
*
|
|
6
|
+
* See docs/site-content/design/2026-04-30-realtime-incremental-stitching.md
|
|
7
|
+
* for the architectural rationale. This file is the type-safe JS
|
|
8
|
+
* wrapper around the RN bridge; `useIncrementalStitcher` is the hook
|
|
9
|
+
* host code consumes; `IncrementalStitcherView` renders the live
|
|
10
|
+
* panorama preview.
|
|
11
|
+
*/
|
|
12
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
|
+
exports.IncrementalOutcome = void 0;
|
|
14
|
+
exports.getIncrementalNativeModule = getIncrementalNativeModule;
|
|
15
|
+
exports.incrementalStitcherIsAvailable = incrementalStitcherIsAvailable;
|
|
16
|
+
exports.cleanupOldKeyframes = cleanupOldKeyframes;
|
|
17
|
+
exports.subscribeIncrementalState = subscribeIncrementalState;
|
|
18
|
+
const react_native_1 = require("react-native");
|
|
19
|
+
/**
|
|
20
|
+
* Per-frame outcome returned by the engine. Mirrors the iOS
|
|
21
|
+
* `RLISFrameOutcome` enum and the Android equivalent — numeric
|
|
22
|
+
* values are kept identical so the JS layer doesn't branch on
|
|
23
|
+
* platform.
|
|
24
|
+
*/
|
|
25
|
+
var IncrementalOutcome;
|
|
26
|
+
(function (IncrementalOutcome) {
|
|
27
|
+
/** High-confidence accept — silent UX. */
|
|
28
|
+
IncrementalOutcome[IncrementalOutcome["AcceptedHigh"] = 0] = "AcceptedHigh";
|
|
29
|
+
/** Accept with marginal confidence — show subtle yellow ring. */
|
|
30
|
+
IncrementalOutcome[IncrementalOutcome["AcceptedMedium"] = 1] = "AcceptedMedium";
|
|
31
|
+
/** Frame too close to the previous accepted — wait for more pan. */
|
|
32
|
+
IncrementalOutcome[IncrementalOutcome["SkippedTooClose"] = 2] = "SkippedTooClose";
|
|
33
|
+
/** Frame too far past the overlap window — show "slow down" hint. */
|
|
34
|
+
IncrementalOutcome[IncrementalOutcome["RejectedTooFar"] = 3] = "RejectedTooFar";
|
|
35
|
+
/** Too few feature matches — show "scene too uniform" hint. */
|
|
36
|
+
IncrementalOutcome[IncrementalOutcome["RejectedSceneUniform"] = 4] = "RejectedSceneUniform";
|
|
37
|
+
/** RANSAC failed or homography degenerate — show "alignment lost". */
|
|
38
|
+
IncrementalOutcome[IncrementalOutcome["RejectedAlignmentLost"] = 5] = "RejectedAlignmentLost";
|
|
39
|
+
/** AR tracking quality was poor — skip silently. */
|
|
40
|
+
IncrementalOutcome[IncrementalOutcome["SkippedTrackingPoor"] = 6] = "SkippedTrackingPoor";
|
|
41
|
+
/**
|
|
42
|
+
* V12.11 Step D — operator panned BACKWARDS past the running
|
|
43
|
+
* max along the pan axis. Engine has SKIPPED the paste; host
|
|
44
|
+
* should auto-finalize the capture (the most useful pano is
|
|
45
|
+
* what we have so far at the high-water mark). Emitted by
|
|
46
|
+
* the rectilinear engine only — cylindrical engines tolerate
|
|
47
|
+
* reverse motion via their warp pipeline.
|
|
48
|
+
*/
|
|
49
|
+
IncrementalOutcome[IncrementalOutcome["RejectedReverseDirection"] = 7] = "RejectedReverseDirection";
|
|
50
|
+
/**
|
|
51
|
+
* V16 — pose-driven keyframe gate rejected the frame because it
|
|
52
|
+
* overlapped >= (1 − overlapThreshold) with the last accepted
|
|
53
|
+
* keyframe. Host should keep showing the same status — user is
|
|
54
|
+
* mid-pan between two natural keyframe boundaries. No UX hint
|
|
55
|
+
* needed (this is the expected behaviour 90% of the time when
|
|
56
|
+
* pose-based selection is on).
|
|
57
|
+
*/
|
|
58
|
+
IncrementalOutcome[IncrementalOutcome["SkippedKeyframeOverlap"] = 8] = "SkippedKeyframeOverlap";
|
|
59
|
+
/**
|
|
60
|
+
* V16 — pose-driven keyframe gate rejected the frame because the
|
|
61
|
+
* capture has hit `keyframeMaxCount` (default 6). Host should
|
|
62
|
+
* auto-finalize since no more frames will be accepted.
|
|
63
|
+
*/
|
|
64
|
+
IncrementalOutcome[IncrementalOutcome["SkippedKeyframeMaxReached"] = 9] = "SkippedKeyframeMaxReached";
|
|
65
|
+
})(IncrementalOutcome || (exports.IncrementalOutcome = IncrementalOutcome = {}));
|
|
66
|
+
/**
|
|
67
|
+
* Lazy-resolve the native module. Returns null on platforms that
|
|
68
|
+
* don't have it registered yet (e.g. older builds without the new
|
|
69
|
+
* native code). Callers fall back to the batch stitcher in that
|
|
70
|
+
* case.
|
|
71
|
+
*/
|
|
72
|
+
function getIncrementalNativeModule() {
|
|
73
|
+
const m = react_native_1.NativeModules['IncrementalStitcher'];
|
|
74
|
+
if (!m || typeof m !== 'object')
|
|
75
|
+
return null;
|
|
76
|
+
// The cast is safe — RN runtime sees only `Function` for each
|
|
77
|
+
// method but TypeScript's structural type system is happy with
|
|
78
|
+
// a record of any-callable.
|
|
79
|
+
return m;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Whether the native incremental stitcher is registered and ready.
|
|
83
|
+
* Equivalent to `getIncrementalNativeModule() !== null`; provided
|
|
84
|
+
* as a convenience export so host code reads cleanly.
|
|
85
|
+
*/
|
|
86
|
+
function incrementalStitcherIsAvailable() {
|
|
87
|
+
return getIncrementalNativeModule() !== null;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* 2026-05-18 (Iss 3) — host-callable helper to clean up old
|
|
91
|
+
* keyframe sessions. Wraps the native `cleanupKeyframes` with a
|
|
92
|
+
* sensible default (24 hours) and a noop fallback when the native
|
|
93
|
+
* method isn't implemented (older SDK builds).
|
|
94
|
+
*
|
|
95
|
+
* Typical use: call this in App.tsx's mount effect or from a
|
|
96
|
+
* background-task hook so storage stays bounded between captures.
|
|
97
|
+
*
|
|
98
|
+
* Resolves with the count of sessions deleted + bytes freed so the
|
|
99
|
+
* host can log / surface a "cleaned up X MB" message. Never
|
|
100
|
+
* rejects — filesystem failures (including ENOENT on the captures
|
|
101
|
+
* dir) resolve as `{ sessionsDeleted: 0, bytesFreed: 0 }`.
|
|
102
|
+
*/
|
|
103
|
+
async function cleanupOldKeyframes(options) {
|
|
104
|
+
const native = getIncrementalNativeModule();
|
|
105
|
+
if (!native?.cleanupKeyframes) {
|
|
106
|
+
return { sessionsDeleted: 0, bytesFreed: 0 };
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
const olderThanMs = options?.olderThanMs ?? 24 * 3600 * 1000;
|
|
110
|
+
return await native.cleanupKeyframes({ olderThanMs });
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
return { sessionsDeleted: 0, bytesFreed: 0 };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Subscribe to per-frame state updates emitted by the native engine.
|
|
118
|
+
* The returned `EmitterSubscription` MUST be removed when no longer
|
|
119
|
+
* needed (`subscription.remove()`); leaks here cause memory growth
|
|
120
|
+
* across captures.
|
|
121
|
+
*/
|
|
122
|
+
function subscribeIncrementalState(listener) {
|
|
123
|
+
const native = getIncrementalNativeModule();
|
|
124
|
+
if (!native)
|
|
125
|
+
return null;
|
|
126
|
+
// Cast through the structural NativeModule type — the bridge
|
|
127
|
+
// module IS an RCTEventEmitter at runtime, which exposes
|
|
128
|
+
// addListener/removeListeners as part of the contract. TS just
|
|
129
|
+
// can't see the iOS side's class hierarchy.
|
|
130
|
+
const emitter = new react_native_1.NativeEventEmitter(react_native_1.NativeModules.IncrementalStitcher);
|
|
131
|
+
return emitter.addListener('IncrementalStateUpdate', listener);
|
|
132
|
+
}
|
|
133
|
+
//# sourceMappingURL=incremental.js.map
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stitchFrames — video / frame stitching API.
|
|
3
|
+
*
|
|
4
|
+
* Implementation status (Phase 2 of #8):
|
|
5
|
+
* - iOS: Swift native module that vendors upstream OpenCV's iOS
|
|
6
|
+
* framework and calls `cv::Stitcher::SCANS` mode (designed for
|
|
7
|
+
* translational shelf captures). Lives in
|
|
8
|
+
* `retailens-capture-sdk/ios/Sources/RNImageStitcher/`.
|
|
9
|
+
* - Android: deferred to Phase 3 — same OpenCV surface, different
|
|
10
|
+
* build (NDK + Gradle). Until that lands, Android calls hit the
|
|
11
|
+
* `StitchNotImplementedError` path below.
|
|
12
|
+
*
|
|
13
|
+
* Why fail loudly instead of falling back to JS?
|
|
14
|
+
* The cloud-sync pipeline depends on a stitched panorama being
|
|
15
|
+
* present. Silently producing a broken or single-frame "panorama"
|
|
16
|
+
* would corrupt downstream SOS computation. Hard-failing here lets
|
|
17
|
+
* the host app surface the unsupported-platform error to the user
|
|
18
|
+
* immediately rather than discovering it on the server hours later.
|
|
19
|
+
*/
|
|
20
|
+
export interface StitchFramesOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Absolute paths to the input image files in capture order.
|
|
23
|
+
* Must share a camera + focal length (we don't blend across sources).
|
|
24
|
+
*/
|
|
25
|
+
framePaths: string[];
|
|
26
|
+
/**
|
|
27
|
+
* Output path for the stitched image (JPEG). Host app chooses the
|
|
28
|
+
* location (tmp vs. cache vs. Documents).
|
|
29
|
+
*/
|
|
30
|
+
outputPath: string;
|
|
31
|
+
/** JPEG quality [0-100]. Default 85. */
|
|
32
|
+
quality?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface StitchFramesResult {
|
|
35
|
+
/** Absolute path to the stitched image on disk. */
|
|
36
|
+
outputPath: string;
|
|
37
|
+
/** Pixel dimensions of the stitched image. */
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
/** Wall-clock ms the stitcher took (host apps log this for perf tracking). */
|
|
41
|
+
durationMs: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stitch ``framePaths`` into a single panoramic image.
|
|
45
|
+
*
|
|
46
|
+
* Throws ``StitchNotImplementedError`` until the native module ships.
|
|
47
|
+
* Callers should catch that specific error and fall back gracefully
|
|
48
|
+
* (e.g. surface a "single-frame mode only" banner in the UI).
|
|
49
|
+
*/
|
|
50
|
+
export declare function stitchFrames(options: StitchFramesOptions): Promise<StitchFramesResult>;
|
|
51
|
+
export declare class StitchNotImplementedError extends Error {
|
|
52
|
+
readonly code = "STITCH_NOT_IMPLEMENTED";
|
|
53
|
+
constructor(message: string);
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=stitchFrames.d.ts.map
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* stitchFrames — video / frame stitching API.
|
|
5
|
+
*
|
|
6
|
+
* Implementation status (Phase 2 of #8):
|
|
7
|
+
* - iOS: Swift native module that vendors upstream OpenCV's iOS
|
|
8
|
+
* framework and calls `cv::Stitcher::SCANS` mode (designed for
|
|
9
|
+
* translational shelf captures). Lives in
|
|
10
|
+
* `retailens-capture-sdk/ios/Sources/RNImageStitcher/`.
|
|
11
|
+
* - Android: deferred to Phase 3 — same OpenCV surface, different
|
|
12
|
+
* build (NDK + Gradle). Until that lands, Android calls hit the
|
|
13
|
+
* `StitchNotImplementedError` path below.
|
|
14
|
+
*
|
|
15
|
+
* Why fail loudly instead of falling back to JS?
|
|
16
|
+
* The cloud-sync pipeline depends on a stitched panorama being
|
|
17
|
+
* present. Silently producing a broken or single-frame "panorama"
|
|
18
|
+
* would corrupt downstream SOS computation. Hard-failing here lets
|
|
19
|
+
* the host app surface the unsupported-platform error to the user
|
|
20
|
+
* immediately rather than discovering it on the server hours later.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.StitchNotImplementedError = void 0;
|
|
24
|
+
exports.stitchFrames = stitchFrames;
|
|
25
|
+
const react_native_1 = require("react-native");
|
|
26
|
+
/**
|
|
27
|
+
* Stitch ``framePaths`` into a single panoramic image.
|
|
28
|
+
*
|
|
29
|
+
* Throws ``StitchNotImplementedError`` until the native module ships.
|
|
30
|
+
* Callers should catch that specific error and fall back gracefully
|
|
31
|
+
* (e.g. surface a "single-frame mode only" banner in the UI).
|
|
32
|
+
*/
|
|
33
|
+
async function stitchFrames(options) {
|
|
34
|
+
// Look for the native module by its canonical name so we can flip
|
|
35
|
+
// this function to "implemented" simply by registering the module
|
|
36
|
+
// in AppDelegate / MainApplication.
|
|
37
|
+
const native = react_native_1.NativeModules['BatchStitcher'];
|
|
38
|
+
if (native && typeof native === 'object' && 'stitch' in native) {
|
|
39
|
+
const fn = native.stitch;
|
|
40
|
+
return fn(options);
|
|
41
|
+
}
|
|
42
|
+
throw new StitchNotImplementedError(`stitchFrames is not yet implemented on ${react_native_1.Platform.OS}. `
|
|
43
|
+
+ 'The react-native-image-stitcher native stitcher module is expected '
|
|
44
|
+
+ 'but not registered — the JS shim is throwing by design so the '
|
|
45
|
+
+ 'host app can fall back to single-frame mode rather than ship '
|
|
46
|
+
+ 'broken panoramas.');
|
|
47
|
+
}
|
|
48
|
+
class StitchNotImplementedError extends Error {
|
|
49
|
+
constructor(message) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.code = 'STITCH_NOT_IMPLEMENTED';
|
|
52
|
+
this.name = 'StitchNotImplementedError';
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
exports.StitchNotImplementedError = StitchNotImplementedError;
|
|
56
|
+
//# sourceMappingURL=stitchFrames.js.map
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* stitchVideo — end-to-end "video → panorama" API.
|
|
3
|
+
*
|
|
4
|
+
* The host app records video while the user holds the shutter; on
|
|
5
|
+
* release we hand the video file path to this function and it
|
|
6
|
+
* returns a single stitched panoramic JPEG. Internally:
|
|
7
|
+
*
|
|
8
|
+
* 1. Native side extracts N evenly-spaced frames from the video
|
|
9
|
+
* (AVAssetImageGenerator on iOS, MediaMetadataRetriever on
|
|
10
|
+
* Android once Phase 3 lands).
|
|
11
|
+
* 2. Native side feeds the frames into `cv::Stitcher::SCANS`.
|
|
12
|
+
* 3. Frames are deleted; only the final panorama remains.
|
|
13
|
+
*
|
|
14
|
+
* The single bridge call keeps the JS thread out of the
|
|
15
|
+
* extract→stitch dance entirely; nothing has to round-trip frame
|
|
16
|
+
* paths back to JS just to hand them to a sibling native call.
|
|
17
|
+
*/
|
|
18
|
+
import type { StitchFramesResult } from './stitchFrames';
|
|
19
|
+
export interface StitchVideoOptions {
|
|
20
|
+
/**
|
|
21
|
+
* Absolute path to the recorded video file. Accepts paths with
|
|
22
|
+
* or without the `file://` prefix — the native module strips it.
|
|
23
|
+
*/
|
|
24
|
+
videoPath: string;
|
|
25
|
+
/**
|
|
26
|
+
* Where the resulting panoramic JPEG should be written.
|
|
27
|
+
*/
|
|
28
|
+
outputPath: string;
|
|
29
|
+
/**
|
|
30
|
+
* Number of frames to sample from the video. Default 10 — the
|
|
31
|
+
* empirical sweet spot for shelf scans on iPhone hardware: enough
|
|
32
|
+
* overlap that homography stays robust, few enough that stitching
|
|
33
|
+
* stays under 4 seconds. Increase only if the user pans further
|
|
34
|
+
* than ~1 m of shelf in a single hold.
|
|
35
|
+
*/
|
|
36
|
+
maxFrames?: number;
|
|
37
|
+
/**
|
|
38
|
+
* JPEG quality [0..100]. Applied to BOTH the intermediate frames
|
|
39
|
+
* (extracted from video) AND the final panorama. Default 85.
|
|
40
|
+
*/
|
|
41
|
+
quality?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Projection used during the warp step. See native side for
|
|
44
|
+
* details. Default `'plane'` — straight verticals/horizontals,
|
|
45
|
+
* good for shelf scans + close-up subjects. Hourglass shape on
|
|
46
|
+
* partial arcs is handled by the rectangular-crop step.
|
|
47
|
+
* - `'plane'`: flat projection. Best for ≤30° pans.
|
|
48
|
+
* - `'cylindrical'`: handles rotational mid-arc pans.
|
|
49
|
+
* - `'spherical'`: full 90°+ panoramic captures.
|
|
50
|
+
*/
|
|
51
|
+
warperType?: 'plane' | 'cylindrical' | 'spherical';
|
|
52
|
+
/**
|
|
53
|
+
* Blending strategy across stitched seams.
|
|
54
|
+
* - `'multiband'` (default): high-quality seams when alignment
|
|
55
|
+
* is good; can produce visible halos when adjacent frames
|
|
56
|
+
* have inconsistent exposure.
|
|
57
|
+
* - `'feather'`: simple alpha-weighted blend; faster + no
|
|
58
|
+
* halo artifacts; slightly more visible seam edge.
|
|
59
|
+
*/
|
|
60
|
+
blenderType?: 'multiband' | 'feather';
|
|
61
|
+
/**
|
|
62
|
+
* Seam finding strategy.
|
|
63
|
+
* - `'graphcut'` (default): cv::detail::GraphCutSeamFinder
|
|
64
|
+
* finds optimal cuts before blending — pairs beautifully
|
|
65
|
+
* with `multiband`, but holds all warped frames in memory
|
|
66
|
+
* simultaneously (higher peak).
|
|
67
|
+
* - `'skip'`: streams warp+feed in a single pass, never
|
|
68
|
+
* holding more than one warped frame. Lower peak memory.
|
|
69
|
+
* Right choice on low-RAM devices or with `feather`.
|
|
70
|
+
*/
|
|
71
|
+
seamFinderType?: 'graphcut' | 'skip';
|
|
72
|
+
/**
|
|
73
|
+
* Phase 5: pose-driven stitching. When present and non-empty,
|
|
74
|
+
* the native stitcher skips features → matching → BundleAdjuster
|
|
75
|
+
* and builds cv::detail::CameraParams directly from each pose's
|
|
76
|
+
* intrinsics + quaternion. Each entry has the shape returned
|
|
77
|
+
* by `NativeModules.RNSARSession.snapshotPoseLog()`:
|
|
78
|
+
*
|
|
79
|
+
* { tx, ty, tz, qx, qy, qz, qw,
|
|
80
|
+
* fx, fy, cx, cy,
|
|
81
|
+
* imageWidth, imageHeight,
|
|
82
|
+
* timestampMs, trackingState }
|
|
83
|
+
*
|
|
84
|
+
* Frames whose closest pose is beyond a 100 ms tolerance are
|
|
85
|
+
* dropped before stitching; if fewer than 2 remain the call
|
|
86
|
+
* rejects with `opencv-failed-1032` so the host can fall back
|
|
87
|
+
* to the feature-matched path (re-call `stitchVideo` without
|
|
88
|
+
* `poses`).
|
|
89
|
+
*/
|
|
90
|
+
poses?: Array<{
|
|
91
|
+
tx: number;
|
|
92
|
+
ty: number;
|
|
93
|
+
tz: number;
|
|
94
|
+
qx: number;
|
|
95
|
+
qy: number;
|
|
96
|
+
qz: number;
|
|
97
|
+
qw: number;
|
|
98
|
+
fx: number;
|
|
99
|
+
fy: number;
|
|
100
|
+
cx: number;
|
|
101
|
+
cy: number;
|
|
102
|
+
imageWidth: number;
|
|
103
|
+
imageHeight: number;
|
|
104
|
+
timestampMs: number;
|
|
105
|
+
trackingState: number;
|
|
106
|
+
}>;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Stitch a recorded video file into a single panoramic JPEG.
|
|
110
|
+
*
|
|
111
|
+
* Throws `StitchNotImplementedError` on platforms where the native
|
|
112
|
+
* stitcher hasn't shipped yet (Android until Phase 3). Throws an
|
|
113
|
+
* Error with a code-like message on stitcher failures the JS layer
|
|
114
|
+
* may want to recover from (see `StitcherError` cases on the native
|
|
115
|
+
* side: `insufficient-frames`, `read-failed`, `write-failed`,
|
|
116
|
+
* `opencv-failed-<code>`).
|
|
117
|
+
*/
|
|
118
|
+
export declare function stitchVideo(options: StitchVideoOptions): Promise<StitchFramesResult>;
|
|
119
|
+
//# sourceMappingURL=stitchVideo.d.ts.map
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* stitchVideo — end-to-end "video → panorama" API.
|
|
5
|
+
*
|
|
6
|
+
* The host app records video while the user holds the shutter; on
|
|
7
|
+
* release we hand the video file path to this function and it
|
|
8
|
+
* returns a single stitched panoramic JPEG. Internally:
|
|
9
|
+
*
|
|
10
|
+
* 1. Native side extracts N evenly-spaced frames from the video
|
|
11
|
+
* (AVAssetImageGenerator on iOS, MediaMetadataRetriever on
|
|
12
|
+
* Android once Phase 3 lands).
|
|
13
|
+
* 2. Native side feeds the frames into `cv::Stitcher::SCANS`.
|
|
14
|
+
* 3. Frames are deleted; only the final panorama remains.
|
|
15
|
+
*
|
|
16
|
+
* The single bridge call keeps the JS thread out of the
|
|
17
|
+
* extract→stitch dance entirely; nothing has to round-trip frame
|
|
18
|
+
* paths back to JS just to hand them to a sibling native call.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.stitchVideo = stitchVideo;
|
|
22
|
+
const react_native_1 = require("react-native");
|
|
23
|
+
const stitchFrames_1 = require("./stitchFrames");
|
|
24
|
+
/**
|
|
25
|
+
* Stitch a recorded video file into a single panoramic JPEG.
|
|
26
|
+
*
|
|
27
|
+
* Throws `StitchNotImplementedError` on platforms where the native
|
|
28
|
+
* stitcher hasn't shipped yet (Android until Phase 3). Throws an
|
|
29
|
+
* Error with a code-like message on stitcher failures the JS layer
|
|
30
|
+
* may want to recover from (see `StitcherError` cases on the native
|
|
31
|
+
* side: `insufficient-frames`, `read-failed`, `write-failed`,
|
|
32
|
+
* `opencv-failed-<code>`).
|
|
33
|
+
*/
|
|
34
|
+
async function stitchVideo(options) {
|
|
35
|
+
const native = react_native_1.NativeModules['BatchStitcher'];
|
|
36
|
+
if (native
|
|
37
|
+
&& typeof native === 'object'
|
|
38
|
+
&& typeof native.stitchVideo === 'function') {
|
|
39
|
+
const fn = native.stitchVideo;
|
|
40
|
+
// 60-second hard deadline. cv::Stitcher::stitch can occasionally
|
|
41
|
+
// grind for minutes inside bundle adjustment on hard inputs (low
|
|
42
|
+
// texture, extreme parallax). A timeout converts those into a
|
|
43
|
+
// clear UI error rather than letting the stitching banner sit
|
|
44
|
+
// forever. Real stitches finish in 2-8 seconds on iPhone with
|
|
45
|
+
// the tuned PANORAMA settings; 60 s is comfortably above that.
|
|
46
|
+
return Promise.race([
|
|
47
|
+
fn(options),
|
|
48
|
+
new Promise((_, reject) => {
|
|
49
|
+
setTimeout(() => reject(new Error('stitch-timeout: stitching took longer than 30 s')), 30000);
|
|
50
|
+
}),
|
|
51
|
+
]);
|
|
52
|
+
}
|
|
53
|
+
throw new stitchFrames_1.StitchNotImplementedError(`stitchVideo is not yet implemented on ${react_native_1.Platform.OS}. `
|
|
54
|
+
+ 'The react-native-image-stitcher native stitcher module is expected '
|
|
55
|
+
+ 'but not registered (or the build predates Phase 2.5 of #8).');
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=stitchVideo.js.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useIncrementalJSDriver — vision-camera + gyro frame driver for
|
|
3
|
+
* the incremental panorama engine, used in non-AR captures on both
|
|
4
|
+
* iOS and Android.
|
|
5
|
+
*
|
|
6
|
+
* History: previously called `useIncrementalAndroidDriver` because
|
|
7
|
+
* it was Android-only. As of 2026-05-17 (Issue #2), the native
|
|
8
|
+
* `processFrameAtPath` entry point exists on both platforms and the
|
|
9
|
+
* hook drives non-AR on iOS too; renamed 2026-05-19 to reflect
|
|
10
|
+
* that.
|
|
11
|
+
*
|
|
12
|
+
* Why this exists
|
|
13
|
+
* In AR captures the engine consumes frames from the ARSession
|
|
14
|
+
* stream natively (60 Hz pose + image delivery, zero JS
|
|
15
|
+
* involvement once started). In NON-AR captures there is no AR
|
|
16
|
+
* session — vision-camera owns the camera — so the engine needs
|
|
17
|
+
* another frame source. This hook fills the gap:
|
|
18
|
+
*
|
|
19
|
+
* - vision-camera keeps the camera viewport
|
|
20
|
+
* - `takeSnapshot()` runs at ~250 ms intervals during press-hold
|
|
21
|
+
* - `react-native-sensors` gyroscope is integrated to estimate
|
|
22
|
+
* cumulative yaw/pitch (drives the FoV-overlap gate)
|
|
23
|
+
* - Each snapshot path + integrated pose is fed to
|
|
24
|
+
* `IncrementalStitcher.processFrameAtPath()`
|
|
25
|
+
*
|
|
26
|
+
* Trade-off vs the AR path
|
|
27
|
+
* Gyro integration drifts ~1–2° per minute. Acceptable for the
|
|
28
|
+
* typical 5–15 s shelf pan; not great for ambitious 360° captures.
|
|
29
|
+
* Snapshot rate is ~4 Hz (vs 60 Hz in AR mode). Pose drives
|
|
30
|
+
* frame-selection only — the actual image alignment is feature-
|
|
31
|
+
* matched + RANSAC-fit, so quality of the panorama itself isn't
|
|
32
|
+
* bounded by gyro accuracy.
|
|
33
|
+
*
|
|
34
|
+
* Lifecycle
|
|
35
|
+
* `start({ cameraRef })` enables the loop; `stop()` tears down.
|
|
36
|
+
* Both should be called by the host's hold-start / hold-complete
|
|
37
|
+
* handlers. Safe to call on either platform; the hook only
|
|
38
|
+
* activates inside the start/stop block.
|
|
39
|
+
*/
|
|
40
|
+
import type { Camera } from 'react-native-vision-camera';
|
|
41
|
+
export interface UseIncrementalJSDriverOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Snapshot interval in ms. Default 250 (≈ 4 Hz). Lower = more
|
|
44
|
+
* candidate frames + more disk I/O. Don't go below 200 — vision-
|
|
45
|
+
* camera's snapshot pipeline can't keep up reliably below that.
|
|
46
|
+
*/
|
|
47
|
+
snapshotIntervalMs?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Gyro sample rate in ms (~30 Hz default matches the existing
|
|
50
|
+
* `PanoramaGuidance` cadence). Used for pose integration only —
|
|
51
|
+
* not the snapshot rate.
|
|
52
|
+
*/
|
|
53
|
+
gyroIntervalMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Approximate horizontal FoV of the device camera. Drives the
|
|
56
|
+
* overlap-percent calculation in the native engine. Default 65°
|
|
57
|
+
* is a reasonable mid-tier smartphone average.
|
|
58
|
+
*/
|
|
59
|
+
fovHorizDegrees?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Approximate vertical FoV of the device camera. Default 50° for
|
|
62
|
+
* typical 4:3 phone cameras. When ARCore-driven path is in use
|
|
63
|
+
* the engine receives both FoVs straight from intrinsics; the
|
|
64
|
+
* gyro driver is a fallback so the defaults are good enough.
|
|
65
|
+
*/
|
|
66
|
+
fovVertDegrees?: number;
|
|
67
|
+
}
|
|
68
|
+
export interface IncrementalJSDriverHandle {
|
|
69
|
+
start: (cameraRef: React.RefObject<Camera | null>) => void;
|
|
70
|
+
stop: () => void;
|
|
71
|
+
isRunning: boolean;
|
|
72
|
+
}
|
|
73
|
+
export declare function useIncrementalJSDriver(options?: UseIncrementalJSDriverOptions): IncrementalJSDriverHandle;
|
|
74
|
+
//# sourceMappingURL=useIncrementalJSDriver.d.ts.map
|