react-native-image-stitcher 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +21 -0
  4. package/README.md +189 -0
  5. package/RNImageStitcher.podspec +76 -0
  6. package/android/build.gradle +224 -0
  7. package/android/src/main/AndroidManifest.xml +3 -0
  8. package/android/src/main/cpp/CMakeLists.txt +124 -0
  9. package/android/src/main/cpp/image_stitcher_jni.cpp +145 -0
  10. package/android/src/main/cpp/keyframe_gate_jni.cpp +204 -0
  11. package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +426 -0
  12. package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +960 -0
  13. package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +2371 -0
  14. package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +256 -0
  15. package/android/src/main/java/io/imagestitcher/rn/QualityChecker.kt +167 -0
  16. package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +39 -0
  17. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +558 -0
  18. package/android/src/main/java/io/imagestitcher/rn/RNSARCameraViewManager.kt +35 -0
  19. package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +784 -0
  20. package/android/src/main/java/io/imagestitcher/rn/ar/BackgroundRenderer.kt +176 -0
  21. package/android/src/main/java/io/imagestitcher/rn/ar/ShaderUtil.kt +67 -0
  22. package/android/src/main/java/io/imagestitcher/rn/ar/YuvImageConverter.kt +201 -0
  23. package/cpp/ar_frame_pose.h +63 -0
  24. package/cpp/keyframe_gate.cpp +927 -0
  25. package/cpp/keyframe_gate.hpp +240 -0
  26. package/cpp/stitcher.cpp +2207 -0
  27. package/cpp/stitcher.hpp +275 -0
  28. package/dist/ar/useARSession.d.ts +102 -0
  29. package/dist/ar/useARSession.js +133 -0
  30. package/dist/camera/ARCameraView.d.ts +93 -0
  31. package/dist/camera/ARCameraView.js +170 -0
  32. package/dist/camera/Camera.d.ts +134 -0
  33. package/dist/camera/Camera.js +688 -0
  34. package/dist/camera/CameraShutter.d.ts +80 -0
  35. package/dist/camera/CameraShutter.js +237 -0
  36. package/dist/camera/CameraView.d.ts +65 -0
  37. package/dist/camera/CameraView.js +117 -0
  38. package/dist/camera/CaptureControlsBar.d.ts +87 -0
  39. package/dist/camera/CaptureControlsBar.js +82 -0
  40. package/dist/camera/CaptureHeader.d.ts +62 -0
  41. package/dist/camera/CaptureHeader.js +81 -0
  42. package/dist/camera/CapturePreview.d.ts +70 -0
  43. package/dist/camera/CapturePreview.js +188 -0
  44. package/dist/camera/CaptureStatusOverlay.d.ts +75 -0
  45. package/dist/camera/CaptureStatusOverlay.js +326 -0
  46. package/dist/camera/CaptureThumbnailStrip.d.ts +87 -0
  47. package/dist/camera/CaptureThumbnailStrip.js +177 -0
  48. package/dist/camera/IncrementalPanGuide.d.ts +83 -0
  49. package/dist/camera/IncrementalPanGuide.js +267 -0
  50. package/dist/camera/PanoramaBandOverlay.d.ts +107 -0
  51. package/dist/camera/PanoramaBandOverlay.js +399 -0
  52. package/dist/camera/PanoramaConfirmModal.d.ts +57 -0
  53. package/dist/camera/PanoramaConfirmModal.js +128 -0
  54. package/dist/camera/PanoramaGuidance.d.ts +79 -0
  55. package/dist/camera/PanoramaGuidance.js +246 -0
  56. package/dist/camera/PanoramaSettingsModal.d.ts +311 -0
  57. package/dist/camera/PanoramaSettingsModal.js +611 -0
  58. package/dist/camera/ViewportCropOverlay.d.ts +46 -0
  59. package/dist/camera/ViewportCropOverlay.js +67 -0
  60. package/dist/camera/useCapture.d.ts +111 -0
  61. package/dist/camera/useCapture.js +160 -0
  62. package/dist/camera/useDeviceOrientation.d.ts +48 -0
  63. package/dist/camera/useDeviceOrientation.js +131 -0
  64. package/dist/camera/useVideoCapture.d.ts +79 -0
  65. package/dist/camera/useVideoCapture.js +151 -0
  66. package/dist/index.d.ts +26 -0
  67. package/dist/index.js +39 -0
  68. package/dist/quality/normaliseOrientation.d.ts +36 -0
  69. package/dist/quality/normaliseOrientation.js +62 -0
  70. package/dist/quality/runQualityCheck.d.ts +41 -0
  71. package/dist/quality/runQualityCheck.js +98 -0
  72. package/dist/sensors/useIMUTranslationGate.d.ts +70 -0
  73. package/dist/sensors/useIMUTranslationGate.js +235 -0
  74. package/dist/stitching/IncrementalStitcherView.d.ts +41 -0
  75. package/dist/stitching/IncrementalStitcherView.js +157 -0
  76. package/dist/stitching/incremental.d.ts +930 -0
  77. package/dist/stitching/incremental.js +133 -0
  78. package/dist/stitching/stitchFrames.d.ts +55 -0
  79. package/dist/stitching/stitchFrames.js +56 -0
  80. package/dist/stitching/stitchVideo.d.ts +119 -0
  81. package/dist/stitching/stitchVideo.js +57 -0
  82. package/dist/stitching/useIncrementalJSDriver.d.ts +74 -0
  83. package/dist/stitching/useIncrementalJSDriver.js +199 -0
  84. package/dist/stitching/useIncrementalStitcher.d.ts +58 -0
  85. package/dist/stitching/useIncrementalStitcher.js +172 -0
  86. package/dist/types.d.ts +58 -0
  87. package/dist/types.js +15 -0
  88. package/ios/Package.swift +72 -0
  89. package/ios/Sources/RNImageStitcher/ARCameraViewManager.m +33 -0
  90. package/ios/Sources/RNImageStitcher/ARCameraViewManager.swift +40 -0
  91. package/ios/Sources/RNImageStitcher/ARSessionBridge.m +55 -0
  92. package/ios/Sources/RNImageStitcher/ARSessionBridge.swift +149 -0
  93. package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +2727 -0
  94. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.m +85 -0
  95. package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +625 -0
  96. package/ios/Sources/RNImageStitcher/KeyframeGate.swift +328 -0
  97. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +141 -0
  98. package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +278 -0
  99. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +473 -0
  100. package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +1326 -0
  101. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +97 -0
  102. package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +296 -0
  103. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +103 -0
  104. package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +3285 -0
  105. package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +238 -0
  106. package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +1880 -0
  107. package/ios/Sources/RNImageStitcher/QualityChecker.swift +252 -0
  108. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.m +26 -0
  109. package/ios/Sources/RNImageStitcher/QualityCheckerBridge.swift +72 -0
  110. package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +114 -0
  111. package/ios/Sources/RNImageStitcher/RNSARSession.swift +1111 -0
  112. package/ios/Sources/RNImageStitcher/Stitcher.swift +243 -0
  113. package/ios/Sources/RNImageStitcher/StitcherBridge.m +28 -0
  114. package/ios/Sources/RNImageStitcher/StitcherBridge.swift +246 -0
  115. package/package.json +73 -0
  116. package/react-native.config.js +34 -0
  117. package/scripts/opencv-version.txt +1 -0
  118. package/scripts/postinstall-fetch-binaries.js +286 -0
  119. package/src/ar/useARSession.ts +210 -0
  120. package/src/camera/.gitkeep +0 -0
  121. package/src/camera/ARCameraView.tsx +256 -0
  122. package/src/camera/Camera.tsx +1053 -0
  123. package/src/camera/CameraShutter.tsx +292 -0
  124. package/src/camera/CameraView.tsx +157 -0
  125. package/src/camera/CaptureControlsBar.tsx +204 -0
  126. package/src/camera/CaptureHeader.tsx +184 -0
  127. package/src/camera/CapturePreview.tsx +318 -0
  128. package/src/camera/CaptureStatusOverlay.tsx +391 -0
  129. package/src/camera/CaptureThumbnailStrip.tsx +277 -0
  130. package/src/camera/IncrementalPanGuide.tsx +328 -0
  131. package/src/camera/PanoramaBandOverlay.tsx +498 -0
  132. package/src/camera/PanoramaConfirmModal.tsx +206 -0
  133. package/src/camera/PanoramaGuidance.tsx +327 -0
  134. package/src/camera/PanoramaSettingsModal.tsx +1357 -0
  135. package/src/camera/ViewportCropOverlay.tsx +81 -0
  136. package/src/camera/useCapture.ts +279 -0
  137. package/src/camera/useDeviceOrientation.ts +140 -0
  138. package/src/camera/useVideoCapture.ts +236 -0
  139. package/src/index.ts +53 -0
  140. package/src/quality/.gitkeep +0 -0
  141. package/src/quality/normaliseOrientation.ts +79 -0
  142. package/src/quality/runQualityCheck.ts +131 -0
  143. package/src/sensors/useIMUTranslationGate.ts +347 -0
  144. package/src/stitching/.gitkeep +0 -0
  145. package/src/stitching/IncrementalStitcherView.tsx +198 -0
  146. package/src/stitching/incremental.ts +1021 -0
  147. package/src/stitching/stitchFrames.ts +88 -0
  148. package/src/stitching/stitchVideo.ts +153 -0
  149. package/src/stitching/useIncrementalJSDriver.ts +273 -0
  150. package/src/stitching/useIncrementalStitcher.ts +252 -0
  151. package/src/types.ts +78 -0
@@ -0,0 +1,252 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * useIncrementalStitcher — React hook driving the live panorama
4
+ * engine.
5
+ *
6
+ * Lifecycle:
7
+ * 1. Host calls `useARSession().start()` to put the AR session in
8
+ * tracking mode. (Works for AR-supported devices only — non-AR
9
+ * fallback comes in a later phase.)
10
+ * 2. Host calls `start()` from this hook. The native engine
11
+ * registers itself as the AR session's frame consumer.
12
+ * 3. Native emits a state event for every ARFrame the engine
13
+ * processes (~60 Hz, mostly trivially-skipped). The hook
14
+ * mirrors this into React state so a `<IncrementalStitcherView>`
15
+ * or any other consumer can render the live panorama + UX hints.
16
+ * 4. Host calls `finalize(outputPath)` when the user releases the
17
+ * shutter; resolves with the final panorama path + stats.
18
+ * 5. Host calls `cancel()` if the user dismisses the capture.
19
+ */
20
+
21
+ import { useCallback, useEffect, useRef, useState } from 'react';
22
+ import {
23
+ getIncrementalNativeModule,
24
+ incrementalStitcherIsAvailable,
25
+ subscribeIncrementalState,
26
+ IncrementalOutcome,
27
+ type IncrementalState,
28
+ type IncrementalStartOptions,
29
+ type IncrementalFinalizeResult,
30
+ } from './incremental';
31
+
32
+
33
+ export type IncrementalHint =
34
+ | 'slow-down'
35
+ | 'scene-uniform'
36
+ | 'alignment-lost'
37
+ | 'tracking-poor'
38
+ | null;
39
+
40
+
41
+ export interface UseIncrementalStitcherReturn {
42
+ /** Whether the native engine is registered. False = no fallback wiring. */
43
+ isAvailable: boolean;
44
+ /** True between successful `start()` and `finalize()`/`cancel()`. */
45
+ isRunning: boolean;
46
+ /** Latest state pushed by the native engine, or null pre-start. */
47
+ state: IncrementalState | null;
48
+ /**
49
+ * Convenience: which UX hint to show, derived from the latest
50
+ * state.outcome. null when nothing should be shown (silent
51
+ * accepts, skips inside the overlap window).
52
+ */
53
+ hint: IncrementalHint;
54
+ /**
55
+ * Convenience: 'high' | 'medium' | null based on the last accept.
56
+ * Drives confidence-ring rendering in the live preview.
57
+ */
58
+ confidenceLevel: 'high' | 'medium' | null;
59
+ /** Begin a new capture. Throws if the AR session isn't running. */
60
+ start: (options?: IncrementalStartOptions) => Promise<void>;
61
+ /**
62
+ * End the capture and write the final panorama. When `outputPath`
63
+ * is omitted or empty, the native side picks a path under the
64
+ * app's tmp directory and returns it in the result.
65
+ *
66
+ * `captureOrientation` (optional) — pass the user's CURRENT
67
+ * device orientation at finalize time. The engine prefers this
68
+ * value over the start-time snapshot for the bake-rotation pass,
69
+ * so cross-orientation captures (user opened screen in portrait,
70
+ * captured in landscape) bake correctly. Omit to keep the legacy
71
+ * behaviour (start-time orientation).
72
+ */
73
+ finalize: (
74
+ outputPath?: string,
75
+ quality?: number,
76
+ captureOrientation?: string,
77
+ ) => Promise<IncrementalFinalizeResult>;
78
+ /** Abort the capture without producing output. */
79
+ cancel: () => Promise<void>;
80
+ }
81
+
82
+
83
+ /**
84
+ * Map raw outcome → user-facing hint string. null = no banner.
85
+ */
86
+ function outcomeToHint(outcome: IncrementalOutcome): IncrementalHint {
87
+ switch (outcome) {
88
+ case IncrementalOutcome.RejectedTooFar:
89
+ return 'slow-down';
90
+ case IncrementalOutcome.RejectedSceneUniform:
91
+ return 'scene-uniform';
92
+ case IncrementalOutcome.RejectedAlignmentLost:
93
+ return 'alignment-lost';
94
+ case IncrementalOutcome.SkippedTrackingPoor:
95
+ return 'tracking-poor';
96
+ case IncrementalOutcome.AcceptedHigh:
97
+ case IncrementalOutcome.AcceptedMedium:
98
+ case IncrementalOutcome.SkippedTooClose:
99
+ default:
100
+ return null;
101
+ }
102
+ }
103
+
104
+
105
+ function outcomeToConfidence(
106
+ outcome: IncrementalOutcome,
107
+ ): 'high' | 'medium' | null {
108
+ if (outcome === IncrementalOutcome.AcceptedHigh) return 'high';
109
+ if (outcome === IncrementalOutcome.AcceptedMedium) return 'medium';
110
+ return null;
111
+ }
112
+
113
+
114
+ export function useIncrementalStitcher(): UseIncrementalStitcherReturn {
115
+ const native = getIncrementalNativeModule();
116
+ const isAvailable = incrementalStitcherIsAvailable();
117
+ const [isRunning, setIsRunning] = useState(false);
118
+ const [state, setState] = useState<IncrementalState | null>(null);
119
+
120
+ // Keep the latest hint/confidence sticky for a few frames after a
121
+ // skip — otherwise the UI flickers since SkippedTooClose returns
122
+ // a "silent" outcome between every accept. We collapse this by
123
+ // only updating hint when the new outcome is itself a hint or an
124
+ // accept, leaving non-hint skips alone.
125
+ const lastHintRef = useRef<IncrementalHint>(null);
126
+
127
+ // Subscribe to native events on mount. The subscription itself
128
+ // is cheap; the native side gates `hasListeners` so events are
129
+ // only emitted when JS is listening.
130
+ useEffect(() => {
131
+ if (!native) return undefined;
132
+ const sub = subscribeIncrementalState((nextState) => {
133
+ // Sticky-snapshot merge: the native side emits a state event
134
+ // for EVERY ARFrame the engine processes (~60 Hz), most of
135
+ // which are SkippedTooClose with NO snapshot path. A naive
136
+ // `setState(nextState)` would wipe the panoramaPath to null
137
+ // 60 times per second, blanking the live preview between
138
+ // accepts. Keep the last-good snapshot fields so the PiP
139
+ // shows the most recent panorama continuously between accepts;
140
+ // other fields (outcome, confidence, hint) update normally.
141
+ setState((prev) => {
142
+ if (!nextState.panoramaPath && prev?.panoramaPath) {
143
+ return {
144
+ ...nextState,
145
+ panoramaPath: prev.panoramaPath,
146
+ width: prev.width,
147
+ height: prev.height,
148
+ };
149
+ }
150
+ return nextState;
151
+ });
152
+ const newHint = outcomeToHint(nextState.outcome);
153
+ if (newHint !== null) {
154
+ lastHintRef.current = newHint;
155
+ } else if (
156
+ nextState.outcome === IncrementalOutcome.AcceptedHigh ||
157
+ nextState.outcome === IncrementalOutcome.AcceptedMedium
158
+ ) {
159
+ // An accept clears any prior hint — operator's back on track.
160
+ lastHintRef.current = null;
161
+ }
162
+ // Else: SkippedTooClose etc. — leave the hint alone.
163
+ });
164
+ return () => sub?.remove();
165
+ }, [native]);
166
+
167
+ const start = useCallback(
168
+ async (options: IncrementalStartOptions = {}) => {
169
+ if (!native) {
170
+ throw new Error(
171
+ 'useIncrementalStitcher: IncrementalStitcher native '
172
+ + 'module is not registered. Ensure the SDK pod has been '
173
+ + 'rebuilt against the host app.',
174
+ );
175
+ }
176
+ await native.start(options);
177
+ setIsRunning(true);
178
+ setState(null);
179
+ lastHintRef.current = null;
180
+ },
181
+ [native],
182
+ );
183
+
184
+ const finalize = useCallback(
185
+ async (
186
+ outputPath?: string,
187
+ quality = 90,
188
+ captureOrientation?: string,
189
+ ): Promise<IncrementalFinalizeResult> => {
190
+ if (!native) {
191
+ throw new Error('useIncrementalStitcher: native module unavailable');
192
+ }
193
+ const result = await native.finalize({
194
+ outputPath: outputPath ?? '',
195
+ quality,
196
+ // 2026-05-18 (iOS cross-orientation fix) — fresh orientation
197
+ // at finalize time. Engine uses this for bake-rotation
198
+ // instead of the start-time snapshot. Undefined = keep
199
+ // legacy start-time behaviour.
200
+ captureOrientation,
201
+ });
202
+ setIsRunning(false);
203
+ // Clear React state on finalize so the next start doesn't
204
+ // briefly show stale frame counts / hint banners from the
205
+ // previous capture. Without this, the IncrementalStitcherView
206
+ // displayed acceptedCount from the prior pan if a late event
207
+ // had already updated state.
208
+ setState(null);
209
+ lastHintRef.current = null;
210
+ return result;
211
+ },
212
+ [native],
213
+ );
214
+
215
+ const cancel = useCallback(async () => {
216
+ if (!native) return;
217
+ await native.cancel();
218
+ setIsRunning(false);
219
+ setState(null);
220
+ lastHintRef.current = null;
221
+ }, [native]);
222
+
223
+ // Cleanup-on-unmount that actually works. The previous version
224
+ // captured `isRunning` from the initial render (false), so the
225
+ // cancel never fired. Reading from a ref keeps the latest
226
+ // value visible at unmount time.
227
+ const isRunningRef = useRef(false);
228
+ useEffect(() => {
229
+ isRunningRef.current = isRunning;
230
+ }, [isRunning]);
231
+ useEffect(() => {
232
+ return () => {
233
+ if (native && isRunningRef.current) {
234
+ native.cancel().catch(() => undefined);
235
+ }
236
+ };
237
+ // eslint-disable-next-line react-hooks/exhaustive-deps
238
+ }, []);
239
+
240
+ const confidenceLevel = state ? outcomeToConfidence(state.outcome) : null;
241
+
242
+ return {
243
+ isAvailable,
244
+ isRunning,
245
+ state,
246
+ hint: lastHintRef.current,
247
+ confidenceLevel,
248
+ start,
249
+ finalize,
250
+ cancel,
251
+ };
252
+ }
package/src/types.ts ADDED
@@ -0,0 +1,78 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * Internal type definitions for `react-native-image-stitcher`.
4
+ *
5
+ * These are NOT re-exported from `src/index.ts` — they are consumed
6
+ * by internal modules (`useCapture`, `runQualityCheck`) and adapted
7
+ * to the public surface (e.g., `CameraCaptureResult` in
8
+ * `src/camera/Camera.tsx`) before reaching consumers.
9
+ *
10
+ * If something here needs to become public, expose it deliberately
11
+ * from `src/index.ts` rather than encouraging deep imports.
12
+ */
13
+
14
+ // ── Quality-check result types ────────────────────────────────────────────
15
+ // These are used by `quality/runQualityCheck.ts` and the internal
16
+ // `useCapture` hook. Algorithm details: Laplacian variance for blur,
17
+ // mean luminance for brightness.
18
+
19
+ export interface QualityThresholds {
20
+ /** Minimum Laplacian variance for blur detection */
21
+ minBlurScore: number;
22
+ /** Minimum brightness (0-255) */
23
+ minBrightness: number;
24
+ /** Maximum brightness (0-255) */
25
+ maxBrightness: number;
26
+ }
27
+
28
+ export interface QualityReport {
29
+ passed: boolean;
30
+ blurScore: number;
31
+ brightnessScore: number;
32
+ issues: QualityIssue[];
33
+ }
34
+
35
+ export interface QualityIssue {
36
+ type: 'blur' | 'brightness_low' | 'brightness_high' | 'framing';
37
+ message: string;
38
+ severity: 'warning' | 'error';
39
+ }
40
+
41
+ // ── Device metadata captured at takePhoto time ────────────────────────────
42
+ // Internal-only. `useCapture` populates this from native side; the
43
+ // public `CameraCaptureResult` (in Camera.tsx) doesn't include it
44
+ // because most public consumers don't want it and shouldn't pay for
45
+ // the round-trip-to-native cost in their type contract.
46
+
47
+ export interface DeviceMetadata {
48
+ platform: 'ios' | 'android';
49
+ osVersion: string;
50
+ deviceModel: string;
51
+ cameraId: string;
52
+ flashEnabled: boolean;
53
+ }
54
+
55
+ // ── Internal CaptureResult shape returned by useCapture.takePhoto ─────────
56
+ // `Camera.tsx` adapts this into the public `CameraCaptureResult` (a
57
+ // discriminated union of photo + panorama) before emitting `onCapture`.
58
+
59
+ export interface CaptureResult {
60
+ /** Unique device-generated UUID */
61
+ deviceUuid: string;
62
+ /** Local file path to compressed image */
63
+ compressedUri: string;
64
+ /** Local file path to original image (if retained) */
65
+ originalUri?: string;
66
+ /** Image width in pixels, after EXIF orientation correction. */
67
+ width: number;
68
+ /** Image height in pixels, after EXIF orientation correction. */
69
+ height: number;
70
+ /** Whether this is a stitched panoramic image */
71
+ isStitched: boolean;
72
+ /** Capture timestamp (ISO 8601) */
73
+ capturedAt: string;
74
+ /** Quality check results (if enabled) */
75
+ qualityReport?: QualityReport;
76
+ /** Device metadata at capture time */
77
+ deviceMetadata: DeviceMetadata;
78
+ }