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,327 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ /**
3
+ * PanoramaGuidance — gyroscope-driven pan-speed indicator for the
4
+ * tap-and-hold panorama flow.
5
+ *
6
+ * ┌──────────────────────────────────────────────────────────┐
7
+ * │ (camera preview) │
8
+ * │ │
9
+ * │ ↓ │ ← portrait + landscape pan
10
+ * │ green / yellow / red │
11
+ * │ │
12
+ * │ "Pan slowly" / "Slow down" / "Too fast" │
13
+ * └──────────────────────────────────────────────────────────┘
14
+ *
15
+ * Why this exists
16
+ * The SCANS-mode stitcher needs ~30–50 % overlap between
17
+ * consecutive frames. At 30 fps, frames are ~33 ms apart, so
18
+ * pan rates above roughly 30°/s (≈ 0.5 rad/s) produce frames
19
+ * the stitcher can't align — and the user finds out only after
20
+ * the post-release "Stitching failed" alert. Real-time feedback
21
+ * prevents that failure mode.
22
+ *
23
+ * What it does
24
+ * - Subscribes to the device gyroscope (react-native-sensors)
25
+ * ONLY while `active` is true; tears down on inactive so the
26
+ * sensor isn't running the rest of the time the screen is up.
27
+ * - Detects portrait vs landscape from window dimensions; the
28
+ * dominant pan axis changes accordingly:
29
+ * portrait → user pans horizontally → we track gyro Y.
30
+ * landscape → user pans vertically → we track gyro X.
31
+ * - Maps the dominant axis's |rad/s| onto a colour scale and a
32
+ * human-readable hint. Defaults are tuned for SCANS but
33
+ * overrideable.
34
+ *
35
+ * Performance
36
+ * The gyroscope fires ~30 Hz. We update an Animated.Value (which
37
+ * updates the colour interpolation on the native driver) and only
38
+ * call setState when the qualitative bucket (good/warn/bad)
39
+ * changes — keeps re-render volume low.
40
+ */
41
+
42
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
43
+ import {
44
+ StyleSheet,
45
+ Text,
46
+ View,
47
+ type StyleProp,
48
+ type ViewStyle,
49
+ } from 'react-native';
50
+ import {
51
+ gyroscope,
52
+ setUpdateIntervalForType,
53
+ SensorTypes,
54
+ } from 'react-native-sensors';
55
+ import type { Subscription } from 'rxjs';
56
+
57
+ import { useDeviceOrientation } from './useDeviceOrientation';
58
+
59
+
60
+ export type PanoramaSpeedBucket = 'good' | 'warn' | 'bad';
61
+
62
+ type PanAxis = 'horizontal' | 'vertical';
63
+
64
+
65
+ export interface PanoramaGuidanceProps {
66
+ /**
67
+ * Subscribe to the gyroscope only while this is true. Typically
68
+ * driven by the host's `statusPhase === 'recording'`.
69
+ */
70
+ active: boolean;
71
+ /**
72
+ * Force the pan axis instead of auto-detecting from window
73
+ * orientation. Useful for hosts that lock orientation but want
74
+ * the user to pan the orthogonal axis.
75
+ *
76
+ * Default: undefined → auto-detect ("horizontal" in portrait,
77
+ * "vertical" in landscape — matches the user's described
78
+ * "pan top-to-bottom in landscape, left-to-right in portrait").
79
+ */
80
+ axis?: PanAxis;
81
+ /**
82
+ * Rotation rates in rad/s defining the speed buckets. Defaults
83
+ * tuned for cv::Stitcher::SCANS at 30 fps with iPhone FOV ≈ 70°:
84
+ * |rate| ≤ goodMax → green ("good")
85
+ * |rate| ≤ warnMax → amber ("slow down a bit")
86
+ * else → red ("too fast")
87
+ */
88
+ goodMaxRadPerSec?: number;
89
+ warnMaxRadPerSec?: number;
90
+ /** Optional hint message overrides. */
91
+ messages?: {
92
+ good?: string;
93
+ warn?: string;
94
+ bad?: string;
95
+ };
96
+ style?: StyleProp<ViewStyle>;
97
+ }
98
+
99
+
100
+ const DEFAULT_GOOD = 0.5;
101
+ const DEFAULT_WARN = 1.0;
102
+
103
+ const COLOR_GOOD = '#34C759';
104
+ const COLOR_WARN = '#FFCC00';
105
+ const COLOR_BAD = '#FF3B30';
106
+
107
+ const DEFAULT_MESSAGES = {
108
+ good: 'Good pace — keep going',
109
+ warn: 'Slow down a bit',
110
+ bad: 'Too fast — slow down',
111
+ };
112
+
113
+
114
+ function bucketFor(
115
+ rate: number,
116
+ good: number,
117
+ warn: number,
118
+ ): PanoramaSpeedBucket {
119
+ const abs = Math.abs(rate);
120
+ if (abs <= good) return 'good';
121
+ if (abs <= warn) return 'warn';
122
+ return 'bad';
123
+ }
124
+
125
+
126
+ function colorFor(bucket: PanoramaSpeedBucket): string {
127
+ switch (bucket) {
128
+ case 'good':
129
+ return COLOR_GOOD;
130
+ case 'warn':
131
+ return COLOR_WARN;
132
+ case 'bad':
133
+ return COLOR_BAD;
134
+ }
135
+ }
136
+
137
+
138
+ export function PanoramaGuidance({
139
+ active,
140
+ axis,
141
+ goodMaxRadPerSec = DEFAULT_GOOD,
142
+ warnMaxRadPerSec = DEFAULT_WARN,
143
+ messages,
144
+ style,
145
+ }: PanoramaGuidanceProps): React.JSX.Element | null {
146
+ // Use the accelerometer-based hook (NOT useWindowDimensions) so
147
+ // we detect physical orientation even though the app is
148
+ // portrait-locked at the OS level.
149
+ const deviceOrientation = useDeviceOrientation();
150
+ const isPortrait =
151
+ deviceOrientation === 'portrait'
152
+ || deviceOrientation === 'portrait-upside-down';
153
+
154
+ // Auto-detect: in portrait the user pans horizontally
155
+ // (left↔right across the rack) → gyro Y axis dominates.
156
+ // In landscape the user pans vertically (up↕down a tall fixture)
157
+ // → gyro X axis dominates.
158
+ const resolvedAxis: PanAxis =
159
+ axis ?? (isPortrait ? 'horizontal' : 'vertical');
160
+
161
+ // Qualitative bucket — drives both the message and (via colour)
162
+ // the arrow tint. Stored in state so a *change* in bucket
163
+ // re-renders, but per-sample updates do NOT.
164
+ const [bucket, setBucket] = useState<PanoramaSpeedBucket>('good');
165
+
166
+ // Last known rotation rate for the dominant axis, kept in a ref
167
+ // to avoid re-rendering every sample. Read by the bucket logic
168
+ // and (indirectly, via colour interpolation) the animated tint.
169
+ const lastBucketRef = useRef<PanoramaSpeedBucket>('good');
170
+
171
+ useEffect(() => {
172
+ if (!active) {
173
+ lastBucketRef.current = 'good';
174
+ setBucket('good');
175
+ return;
176
+ }
177
+
178
+ // Sample at 33 ms (~30 Hz) — matches the typical recording
179
+ // frame rate so each gyro sample maps to one frame's pan.
180
+ setUpdateIntervalForType(SensorTypes.gyroscope, 33);
181
+
182
+ let subscription: Subscription | null = gyroscope.subscribe({
183
+ next: ({ x, y }) => {
184
+ const rate = resolvedAxis === 'horizontal' ? y : x;
185
+ const next = bucketFor(rate, goodMaxRadPerSec, warnMaxRadPerSec);
186
+ if (next !== lastBucketRef.current) {
187
+ lastBucketRef.current = next;
188
+ setBucket(next);
189
+ }
190
+ },
191
+ error: (err) => {
192
+ // eslint-disable-next-line no-console
193
+ console.warn('[PanoramaGuidance] gyroscope error', err);
194
+ },
195
+ });
196
+
197
+ return () => {
198
+ subscription?.unsubscribe();
199
+ subscription = null;
200
+ };
201
+ }, [active, resolvedAxis, goodMaxRadPerSec, warnMaxRadPerSec]);
202
+
203
+ const resolvedMessages = useMemo(
204
+ () => ({
205
+ good: messages?.good ?? DEFAULT_MESSAGES.good,
206
+ warn: messages?.warn ?? DEFAULT_MESSAGES.warn,
207
+ bad: messages?.bad ?? DEFAULT_MESSAGES.bad,
208
+ }),
209
+ [messages],
210
+ );
211
+
212
+ if (!active) return null;
213
+
214
+ const tint = colorFor(bucket);
215
+ const message = resolvedMessages[bucket];
216
+ // Arrow glyph for the dominant axis. The arrow renders a
217
+ // pannable direction hint — landscape gets a vertical arrow
218
+ // (the user's panning that way), portrait gets a horizontal
219
+ // arrow.
220
+ const arrow = resolvedAxis === 'horizontal' ? '↔' : '↕';
221
+
222
+ // Place the pill at user-perceived bottom across all four
223
+ // orientations. Same pattern as <CaptureStatusOverlay> — the
224
+ // app layout is portrait-locked so we re-position via absolute
225
+ // coords + apply a rotation transform.
226
+ const pillOrientationStyle =
227
+ pillStyleForOrientation(deviceOrientation);
228
+
229
+ return (
230
+ <View
231
+ pointerEvents="none"
232
+ style={[styles.root, pillOrientationStyle, style]}
233
+ accessibilityRole="alert"
234
+ accessibilityLiveRegion="polite"
235
+ >
236
+ <View style={[styles.pill, { borderColor: tint }]}>
237
+ <Text style={[styles.arrow, { color: tint }]}>{arrow}</Text>
238
+ <Text style={[styles.message, { color: tint }]} numberOfLines={1}>
239
+ {message}
240
+ </Text>
241
+ </View>
242
+ </View>
243
+ );
244
+ }
245
+
246
+
247
+ /**
248
+ * Mirror of bannerStyleForOrientation in CaptureStatusOverlay,
249
+ * but anchored at user-perceived BOTTOM instead of TOP.
250
+ */
251
+ function pillStyleForOrientation(
252
+ orientation:
253
+ | 'portrait'
254
+ | 'portrait-upside-down'
255
+ | 'landscape-left'
256
+ | 'landscape-right',
257
+ ): {
258
+ top?: number;
259
+ bottom?: number;
260
+ left?: number;
261
+ right?: number;
262
+ alignItems?: 'center' | 'flex-start' | 'flex-end';
263
+ justifyContent?: 'center' | 'flex-start' | 'flex-end';
264
+ transform?: { rotate: string }[];
265
+ } {
266
+ switch (orientation) {
267
+ case 'landscape-left':
268
+ return {
269
+ top: 0,
270
+ bottom: 0,
271
+ left: 8,
272
+ alignItems: 'flex-start',
273
+ justifyContent: 'center',
274
+ transform: [{ rotate: '90deg' }],
275
+ };
276
+ case 'landscape-right':
277
+ return {
278
+ top: 0,
279
+ bottom: 0,
280
+ right: 8,
281
+ alignItems: 'flex-end',
282
+ justifyContent: 'center',
283
+ transform: [{ rotate: '-90deg' }],
284
+ };
285
+ case 'portrait-upside-down':
286
+ return {
287
+ top: 24,
288
+ left: 0,
289
+ right: 0,
290
+ alignItems: 'center',
291
+ transform: [{ rotate: '180deg' }],
292
+ };
293
+ case 'portrait':
294
+ default:
295
+ return {
296
+ bottom: 24,
297
+ left: 0,
298
+ right: 0,
299
+ alignItems: 'center',
300
+ };
301
+ }
302
+ }
303
+
304
+
305
+ const styles = StyleSheet.create({
306
+ root: {
307
+ position: 'absolute',
308
+ },
309
+ pill: {
310
+ flexDirection: 'row',
311
+ alignItems: 'center',
312
+ paddingHorizontal: 14,
313
+ paddingVertical: 8,
314
+ borderRadius: 24,
315
+ borderWidth: 2,
316
+ backgroundColor: 'rgba(0,0,0,0.55)',
317
+ },
318
+ arrow: {
319
+ fontSize: 22,
320
+ fontWeight: '700',
321
+ marginRight: 8,
322
+ },
323
+ message: {
324
+ fontSize: 14,
325
+ fontWeight: '600',
326
+ },
327
+ });