react-native-image-stitcher 0.14.2 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +164 -0
- package/README.md +35 -0
- package/RNImageStitcher.podspec +8 -7
- package/android/build.gradle +0 -16
- package/android/src/main/cpp/CMakeLists.txt +2 -63
- package/android/src/main/cpp/image_stitcher_jni.cpp +14 -0
- package/android/src/main/cpp/keyframe_gate_jni.cpp +13 -0
- package/android/src/main/java/io/imagestitcher/rn/BatchStitcher.kt +285 -3
- package/android/src/main/java/io/imagestitcher/rn/IncrementalStitcher.kt +180 -1162
- package/android/src/main/java/io/imagestitcher/rn/KeyframeGate.kt +29 -0
- package/android/src/main/java/io/imagestitcher/rn/RNImageStitcherPackage.kt +0 -4
- package/android/src/main/java/io/imagestitcher/rn/RNSARCameraView.kt +129 -71
- package/android/src/main/java/io/imagestitcher/rn/RNSARSession.kt +49 -0
- package/cpp/keyframe_gate.cpp +82 -23
- package/cpp/keyframe_gate.hpp +31 -2
- package/cpp/stitcher.cpp +208 -28
- package/cpp/tests/CMakeLists.txt +18 -12
- package/cpp/tests/keyframe_timebudget_test.cpp +65 -0
- package/cpp/tests/warp_guard_test.cpp +48 -0
- package/cpp/warp_guard.hpp +41 -0
- package/dist/camera/Camera.d.ts +31 -16
- package/dist/camera/Camera.js +11 -3
- package/dist/camera/CameraView.js +93 -3
- package/dist/camera/CaptureStitchStatsToast.d.ts +15 -2
- package/dist/camera/CaptureStitchStatsToast.js +27 -7
- package/dist/camera/PanoramaSettings.d.ts +10 -223
- package/dist/camera/PanoramaSettings.js +6 -28
- package/dist/camera/PanoramaSettingsBridge.d.ts +1 -24
- package/dist/camera/PanoramaSettingsBridge.js +3 -102
- package/dist/camera/PanoramaSettingsModal.js +7 -1
- package/dist/camera/buildPanoramaInitialSettings.d.ts +11 -0
- package/dist/camera/buildPanoramaInitialSettings.js +4 -0
- package/dist/camera/cameraErrorMessages.d.ts +32 -0
- package/dist/camera/cameraErrorMessages.js +53 -0
- package/dist/camera/selectCaptureDevice.d.ts +5 -1
- package/dist/camera/selectCaptureDevice.js +22 -2
- package/dist/camera/useCapture.js +38 -0
- package/dist/index.d.ts +5 -8
- package/dist/index.js +11 -34
- package/dist/stitching/incremental.d.ts +1 -117
- package/dist/stitching/stitchVideo.d.ts +0 -35
- package/dist/types.d.ts +0 -87
- package/ios/Sources/RNImageStitcher/IncrementalStitcher.swift +96 -674
- package/ios/Sources/RNImageStitcher/IncrementalStitcherBridge.swift +9 -12
- package/ios/Sources/RNImageStitcher/KeyframeGate.swift +14 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.h +7 -0
- package/ios/Sources/RNImageStitcher/KeyframeGateBridge.mm +6 -0
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.h +2 -2
- package/ios/Sources/RNImageStitcher/OpenCVKeyframeCollector.mm +3 -3
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.h +28 -60
- package/ios/Sources/RNImageStitcher/OpenCVStitcher.mm +180 -921
- package/ios/Sources/RNImageStitcher/RNSARCameraView.swift +82 -7
- package/ios/Sources/RNImageStitcher/RNSARSession.swift +10 -35
- package/ios/Sources/RNImageStitcher/Stitcher.swift +84 -35
- package/ios/Sources/RNImageStitcher/StitcherBridge.m +13 -0
- package/ios/Sources/RNImageStitcher/StitcherBridge.swift +132 -5
- package/package.json +3 -2
- package/src/camera/Camera.tsx +44 -23
- package/src/camera/CameraView.tsx +113 -4
- package/src/camera/CaptureStitchStatsToast.tsx +58 -14
- package/src/camera/PanoramaSettings.ts +16 -289
- package/src/camera/PanoramaSettingsBridge.ts +3 -114
- package/src/camera/PanoramaSettingsModal.tsx +14 -1
- package/src/camera/__tests__/PanoramaSettingsBridge.test.ts +3 -188
- package/src/camera/__tests__/buildPanoramaInitialSettings.test.ts +41 -0
- package/src/camera/__tests__/cameraErrorMessages.test.ts +76 -0
- package/src/camera/__tests__/selectCaptureDevice.test.ts +33 -0
- package/src/camera/buildPanoramaInitialSettings.ts +17 -0
- package/src/camera/cameraErrorMessages.ts +84 -0
- package/src/camera/selectCaptureDevice.ts +28 -3
- package/src/camera/useCapture.ts +44 -1
- package/src/index.ts +11 -40
- package/src/stitching/incremental.ts +3 -140
- package/src/stitching/stitchVideo.ts +0 -26
- package/src/types.ts +0 -95
- package/android/src/main/cpp/stitcher_jsi_install_jni.cpp +0 -227
- package/android/src/main/java/io/imagestitcher/rn/IncrementalFirstwinsEngine.kt +0 -1081
- package/android/src/main/java/io/imagestitcher/rn/StitcherJsiInstallerModule.kt +0 -103
- package/android/src/main/java/io/imagestitcher/rn/StitcherWorkletRuntime.kt +0 -256
- package/cpp/stitcher_frame_jsi.cpp +0 -214
- package/cpp/stitcher_frame_jsi.hpp +0 -108
- package/cpp/stitcher_proxy_jsi.cpp +0 -109
- package/cpp/stitcher_proxy_jsi.hpp +0 -46
- package/cpp/stitcher_worklet_dispatch.cpp +0 -103
- package/cpp/stitcher_worklet_dispatch.hpp +0 -71
- package/cpp/stitcher_worklet_registry.cpp +0 -91
- package/cpp/stitcher_worklet_registry.hpp +0 -146
- package/cpp/tests/stitcher_worklet_registry_test.cpp +0 -195
- package/dist/stitching/IncrementalStitcherView.d.ts +0 -41
- package/dist/stitching/IncrementalStitcherView.js +0 -157
- package/dist/stitching/StitcherWorkletRegistry.d.ts +0 -117
- package/dist/stitching/StitcherWorkletRegistry.js +0 -78
- package/dist/stitching/ensureStitcherProxyInstalled.d.ts +0 -8
- package/dist/stitching/ensureStitcherProxyInstalled.js +0 -81
- package/dist/stitching/useFrameProcessor.d.ts +0 -119
- package/dist/stitching/useFrameProcessor.js +0 -196
- package/dist/stitching/useFrameStream.d.ts +0 -34
- package/dist/stitching/useFrameStream.js +0 -234
- package/dist/stitching/useThrottledFrameProcessor.d.ts +0 -33
- package/dist/stitching/useThrottledFrameProcessor.js +0 -132
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.h +0 -474
- package/ios/Sources/RNImageStitcher/OpenCVIncrementalStitcher.mm +0 -1328
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.h +0 -103
- package/ios/Sources/RNImageStitcher/OpenCVSlitScanStitcher.mm +0 -3285
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.h +0 -128
- package/ios/Sources/RNImageStitcher/RNSARWorkletRuntime.mm +0 -313
- package/ios/Sources/RNImageStitcher/SaveFrameAsJpegPlugin.mm +0 -185
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.h +0 -60
- package/ios/Sources/RNImageStitcher/StitcherFrameHostObject.mm +0 -214
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.h +0 -42
- package/ios/Sources/RNImageStitcher/StitcherJsiInstaller.mm +0 -160
- package/src/stitching/IncrementalStitcherView.tsx +0 -198
- package/src/stitching/StitcherWorkletRegistry.ts +0 -156
- package/src/stitching/__tests__/StitcherWorkletRegistry.test.ts +0 -176
- package/src/stitching/__tests__/ensureStitcherProxyInstalled.test.ts +0 -94
- package/src/stitching/__tests__/useThrottledFrameProcessor.test.ts +0 -178
- package/src/stitching/ensureStitcherProxyInstalled.ts +0 -141
- package/src/stitching/useFrameProcessor.ts +0 -226
- package/src/stitching/useFrameStream.ts +0 -271
- package/src/stitching/useThrottledFrameProcessor.ts +0 -145
|
@@ -19,10 +19,23 @@
|
|
|
19
19
|
* UI can still use it as their building block.
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import React, {
|
|
23
|
-
|
|
22
|
+
import React, {
|
|
23
|
+
forwardRef,
|
|
24
|
+
useCallback,
|
|
25
|
+
useImperativeHandle,
|
|
26
|
+
useRef,
|
|
27
|
+
useState,
|
|
28
|
+
} from 'react';
|
|
29
|
+
import {
|
|
30
|
+
StyleSheet,
|
|
31
|
+
Text,
|
|
32
|
+
View,
|
|
33
|
+
type LayoutChangeEvent,
|
|
34
|
+
type ViewStyle,
|
|
35
|
+
} from 'react-native';
|
|
24
36
|
import {
|
|
25
37
|
Camera,
|
|
38
|
+
useCameraFormat,
|
|
26
39
|
type CameraDevice,
|
|
27
40
|
type CameraProps,
|
|
28
41
|
} from 'react-native-vision-camera';
|
|
@@ -140,6 +153,39 @@ export const CameraView = forwardRef<Camera | null, CameraViewProps>(function Ca
|
|
|
140
153
|
const innerRef = useRef<Camera>(null);
|
|
141
154
|
useImperativeHandle(ref, () => innerRef.current as Camera);
|
|
142
155
|
|
|
156
|
+
// ── WYSIWYG letterboxing ────────────────────────────────────────
|
|
157
|
+
//
|
|
158
|
+
// Pin BOTH the photo and the preview (video) stream to a 4:3 aspect
|
|
159
|
+
// ratio so the viewport shows exactly what gets captured. Without a
|
|
160
|
+
// pinned format, vision-camera picks the device default for each —
|
|
161
|
+
// commonly a 4:3 photo but a 16:9 preview — so the preview and the
|
|
162
|
+
// saved frame frame different scenes. 4:3 is the native still
|
|
163
|
+
// aspect on essentially every phone camera (incl. ultra-wide), so a
|
|
164
|
+
// matching format is virtually always available; `useCameraFormat`
|
|
165
|
+
// returns the closest match and never throws.
|
|
166
|
+
const format = useCameraFormat(device ?? undefined, [
|
|
167
|
+
{ photoAspectRatio: 4 / 3 },
|
|
168
|
+
{ videoAspectRatio: 4 / 3 },
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
// Measured size of our container, so we can size the <Camera> view to
|
|
172
|
+
// the largest box of the capture's aspect ratio that fits inside it
|
|
173
|
+
// (the rest becomes the black letterbox). We deliberately size the
|
|
174
|
+
// VIEW rather than relying on vision-camera's `resizeMode` alone:
|
|
175
|
+
// resizeMode maps to PreviewView.ScaleType on Android, which several
|
|
176
|
+
// devices ignore under the default SurfaceView compositor — so the
|
|
177
|
+
// preview kept filling the screen. When the view's own aspect ratio
|
|
178
|
+
// equals the feed's, there is nothing left to crop on any platform.
|
|
179
|
+
const [size, setSize] = useState<{ w: number; h: number } | null>(null);
|
|
180
|
+
const onRootLayout = useCallback((e: LayoutChangeEvent) => {
|
|
181
|
+
const { width, height } = e.nativeEvent.layout;
|
|
182
|
+
setSize((prev) =>
|
|
183
|
+
prev && prev.w === width && prev.h === height
|
|
184
|
+
? prev
|
|
185
|
+
: { w: width, h: height },
|
|
186
|
+
);
|
|
187
|
+
}, []);
|
|
188
|
+
|
|
143
189
|
if (!device) {
|
|
144
190
|
return (
|
|
145
191
|
<View style={[styles.placeholder, style]} accessibilityLabel="Camera initialising">
|
|
@@ -148,15 +194,49 @@ export const CameraView = forwardRef<Camera | null, CameraViewProps>(function Ca
|
|
|
148
194
|
);
|
|
149
195
|
}
|
|
150
196
|
|
|
197
|
+
// Capture aspect ratio (W÷H) in the sensor's native landscape
|
|
198
|
+
// orientation (so > 1). Falls back to 4:3 until the format resolves.
|
|
199
|
+
const sensorAspect =
|
|
200
|
+
format && format.photoWidth > 0 && format.photoHeight > 0
|
|
201
|
+
? format.photoWidth / format.photoHeight
|
|
202
|
+
: 4 / 3;
|
|
203
|
+
|
|
204
|
+
// With outputOrientation="device", a portrait device displays the
|
|
205
|
+
// scene rotated, so the on-screen content aspect is the inverse of
|
|
206
|
+
// the landscape sensor aspect. Detect portrait from the measured
|
|
207
|
+
// container — robust across devices, split-screen and rotation.
|
|
208
|
+
const isPortrait = size != null ? size.h >= size.w : true;
|
|
209
|
+
const contentAspect = isPortrait ? 1 / sensorAspect : sensorAspect;
|
|
210
|
+
|
|
211
|
+
// Largest box of `contentAspect` that fits the container, centred by
|
|
212
|
+
// styles.root. The remaining area is the black letterbox. Before the
|
|
213
|
+
// first onLayout we fill the container so the camera session mounts
|
|
214
|
+
// immediately; the exact box snaps in ~1 frame later.
|
|
215
|
+
let cameraStyle: ViewStyle;
|
|
216
|
+
if (size == null || size.w === 0 || size.h === 0) {
|
|
217
|
+
cameraStyle = StyleSheet.absoluteFillObject;
|
|
218
|
+
} else {
|
|
219
|
+
const heightIfFullWidth = size.w / contentAspect;
|
|
220
|
+
cameraStyle =
|
|
221
|
+
heightIfFullWidth <= size.h
|
|
222
|
+
? { width: size.w, height: heightIfFullWidth }
|
|
223
|
+
: { width: size.h * contentAspect, height: size.h };
|
|
224
|
+
}
|
|
225
|
+
|
|
151
226
|
return (
|
|
152
|
-
<View style={[styles.root, style]}>
|
|
227
|
+
<View style={[styles.root, style]} onLayout={onRootLayout}>
|
|
153
228
|
<Camera
|
|
154
229
|
ref={innerRef}
|
|
155
|
-
|
|
230
|
+
// Sized to the letterboxed box (capture aspect ratio) so the
|
|
231
|
+
// preview never crops; styles.root centres it and paints the
|
|
232
|
+
// surrounding bars black. See the cameraStyle computation above.
|
|
233
|
+
style={cameraStyle}
|
|
156
234
|
device={device}
|
|
157
235
|
isActive={isActive}
|
|
158
236
|
photo
|
|
159
237
|
video={video}
|
|
238
|
+
// Pin preview + photo to the same 4:3 format (WYSIWYG capture).
|
|
239
|
+
format={format}
|
|
160
240
|
// v0.13.2 — multi-cam lens switch via zoom (undefined = default).
|
|
161
241
|
{...(zoom != null ? { zoom } : {})}
|
|
162
242
|
// Bake the device orientation into the captured pixels.
|
|
@@ -169,6 +249,25 @@ export const CameraView = forwardRef<Camera | null, CameraViewProps>(function Ca
|
|
|
169
249
|
// how the user is holding the phone, so the saved JPEG is
|
|
170
250
|
// "what you see is what was taken".
|
|
171
251
|
outputOrientation="device"
|
|
252
|
+
// Show the full camera FOV — no cropping. 'contain' maps to
|
|
253
|
+
// AVLayerVideoGravity.resizeAspect on iOS and the equivalent
|
|
254
|
+
// on Android, letterboxing the preview to the sensor's exact
|
|
255
|
+
// aspect ratio. Without this the default 'cover' crops
|
|
256
|
+
// ~19% off each horizontal edge in portrait mode (4:3 sensor
|
|
257
|
+
// in a 9:21 viewport), so the stitcher receives frames the
|
|
258
|
+
// user never saw. Black bars fill the remainder; backgroundColor
|
|
259
|
+
// on styles.root ensures they are always black.
|
|
260
|
+
resizeMode="contain"
|
|
261
|
+
// Android: force TextureView rendering so that FIT_CENTER
|
|
262
|
+
// (the Android equivalent of resizeMode="contain") actually
|
|
263
|
+
// produces visible letterboxing. The default SurfaceView mode
|
|
264
|
+
// composes at the hardware layer below the View hierarchy and
|
|
265
|
+
// on many devices ignores FIT_CENTER, filling the full surface
|
|
266
|
+
// instead. TextureView is part of the regular View hierarchy
|
|
267
|
+
// so the matrix transform for FIT_CENTER works correctly —
|
|
268
|
+
// the bars outside the letterboxed area are transparent,
|
|
269
|
+
// revealing the parent's black backgroundColor.
|
|
270
|
+
androidPreviewViewType="texture-view"
|
|
172
271
|
torch={flash === 'on' ? 'on' : 'off'}
|
|
173
272
|
onError={handleVcError}
|
|
174
273
|
{...cameraProps}
|
|
@@ -189,6 +288,16 @@ const styles = StyleSheet.create({
|
|
|
189
288
|
root: {
|
|
190
289
|
flex: 1,
|
|
191
290
|
overflow: 'hidden',
|
|
291
|
+
// Centre the letterboxed <Camera> box so the black bars are
|
|
292
|
+
// symmetric on both sides (top/bottom in portrait, left/right in
|
|
293
|
+
// landscape).
|
|
294
|
+
alignItems: 'center',
|
|
295
|
+
justifyContent: 'center',
|
|
296
|
+
// Black bars when the camera's aspect ratio doesn't fill the
|
|
297
|
+
// container (e.g. 4:3 sensor in a 9:21 portrait viewport). Without
|
|
298
|
+
// this the bars are transparent, revealing whatever is behind the
|
|
299
|
+
// component.
|
|
300
|
+
backgroundColor: '#000',
|
|
192
301
|
},
|
|
193
302
|
placeholder: {
|
|
194
303
|
flex: 1,
|
|
@@ -23,28 +23,47 @@ import type { IncrementalFinalizeResult } from '../stitching/incremental';
|
|
|
23
23
|
export interface CaptureStitchStatsToastProps {
|
|
24
24
|
/** Toast message to show. Pass null to hide. */
|
|
25
25
|
message: string | null;
|
|
26
|
+
/**
|
|
27
|
+
* Optional bold title rendered above the message (e.g. an action ask
|
|
28
|
+
* like "Pan more slowly"). Omit for a plain single-line toast.
|
|
29
|
+
*/
|
|
30
|
+
title?: string | null;
|
|
26
31
|
/** Top inset for safe-area placement. Toast pinned `topInset + 12`. */
|
|
27
32
|
topInset?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Vertical placement. 'top' (default) pins it `topInset + 12` from the
|
|
35
|
+
* top; 'center' vertically centers it — more prominent, and dodges the
|
|
36
|
+
* notch / Dynamic Island entirely.
|
|
37
|
+
*/
|
|
38
|
+
placement?: 'top' | 'center';
|
|
28
39
|
}
|
|
29
40
|
|
|
30
41
|
export function CaptureStitchStatsToast({
|
|
31
42
|
message,
|
|
43
|
+
title = null,
|
|
32
44
|
topInset = 0,
|
|
45
|
+
placement = 'top',
|
|
33
46
|
}: CaptureStitchStatsToastProps): React.JSX.Element | null {
|
|
34
47
|
if (message === null) return null;
|
|
35
48
|
return (
|
|
36
49
|
<View
|
|
37
50
|
pointerEvents="none"
|
|
38
|
-
style={
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
51
|
+
style={
|
|
52
|
+
placement === 'center'
|
|
53
|
+
? styles.wrapCenter
|
|
54
|
+
: [styles.wrap, { top: topInset + 12 }]
|
|
55
|
+
}
|
|
42
56
|
>
|
|
43
57
|
<View
|
|
44
58
|
style={styles.capsule}
|
|
45
59
|
accessibilityRole="alert"
|
|
46
60
|
accessibilityLiveRegion="polite"
|
|
47
61
|
>
|
|
62
|
+
{title ? (
|
|
63
|
+
<Text style={styles.title} numberOfLines={2}>
|
|
64
|
+
{title}
|
|
65
|
+
</Text>
|
|
66
|
+
) : null}
|
|
48
67
|
<Text style={styles.text} numberOfLines={3}>
|
|
49
68
|
{message}
|
|
50
69
|
</Text>
|
|
@@ -61,6 +80,16 @@ const styles = StyleSheet.create({
|
|
|
61
80
|
alignItems: 'center',
|
|
62
81
|
zIndex: 110,
|
|
63
82
|
},
|
|
83
|
+
wrapCenter: {
|
|
84
|
+
position: 'absolute',
|
|
85
|
+
top: 0,
|
|
86
|
+
bottom: 0,
|
|
87
|
+
left: 24,
|
|
88
|
+
right: 24,
|
|
89
|
+
alignItems: 'center',
|
|
90
|
+
justifyContent: 'center',
|
|
91
|
+
zIndex: 110,
|
|
92
|
+
},
|
|
64
93
|
capsule: {
|
|
65
94
|
paddingHorizontal: 16,
|
|
66
95
|
paddingVertical: 10,
|
|
@@ -68,6 +97,13 @@ const styles = StyleSheet.create({
|
|
|
68
97
|
backgroundColor: 'rgba(15, 23, 42, 0.92)',
|
|
69
98
|
maxWidth: '100%',
|
|
70
99
|
},
|
|
100
|
+
title: {
|
|
101
|
+
color: '#ffffff',
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
fontWeight: '700',
|
|
104
|
+
textAlign: 'center',
|
|
105
|
+
marginBottom: 3,
|
|
106
|
+
},
|
|
71
107
|
text: {
|
|
72
108
|
color: '#ffffff',
|
|
73
109
|
fontSize: 13,
|
|
@@ -93,7 +129,9 @@ const styles = StyleSheet.create({
|
|
|
93
129
|
*/
|
|
94
130
|
export interface UseStitchStatsToastReturn {
|
|
95
131
|
message: string | null;
|
|
96
|
-
|
|
132
|
+
/** Optional bold title shown above `message` (pass to the toast). */
|
|
133
|
+
title: string | null;
|
|
134
|
+
showFor: (msg: string, ms?: number, title?: string) => void;
|
|
97
135
|
showResult: (result: IncrementalFinalizeResult, ms?: number) => void;
|
|
98
136
|
}
|
|
99
137
|
|
|
@@ -101,16 +139,22 @@ const DEFAULT_DISMISS_MS = 4500;
|
|
|
101
139
|
|
|
102
140
|
export function useStitchStatsToast(): UseStitchStatsToastReturn {
|
|
103
141
|
const [message, setMessage] = useState<string | null>(null);
|
|
142
|
+
const [title, setTitle] = useState<string | null>(null);
|
|
104
143
|
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
105
144
|
|
|
106
|
-
const showFor = useCallback(
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
setMessage(
|
|
111
|
-
timerRef.current =
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
const showFor = useCallback(
|
|
146
|
+
(msg: string, ms = DEFAULT_DISMISS_MS, titleText?: string) => {
|
|
147
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
148
|
+
setTitle(titleText ?? null);
|
|
149
|
+
setMessage(msg);
|
|
150
|
+
timerRef.current = setTimeout(() => {
|
|
151
|
+
setMessage(null);
|
|
152
|
+
setTitle(null);
|
|
153
|
+
timerRef.current = null;
|
|
154
|
+
}, ms);
|
|
155
|
+
},
|
|
156
|
+
[],
|
|
157
|
+
);
|
|
114
158
|
|
|
115
159
|
const showResult = useCallback(
|
|
116
160
|
(result: IncrementalFinalizeResult, ms = DEFAULT_DISMISS_MS) => {
|
|
@@ -151,5 +195,5 @@ export function useStitchStatsToast(): UseStitchStatsToastReturn {
|
|
|
151
195
|
if (timerRef.current) clearTimeout(timerRef.current);
|
|
152
196
|
}, []);
|
|
153
197
|
|
|
154
|
-
return { message, showFor, showResult };
|
|
198
|
+
return { message, title, showFor, showResult };
|
|
155
199
|
}
|
|
@@ -203,6 +203,17 @@ export interface FrameSelectionSettings {
|
|
|
203
203
|
*/
|
|
204
204
|
overlapThreshold: number;
|
|
205
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Time-budget force-accept (BOTH strategies, AR + non-AR). When > 0,
|
|
208
|
+
* the gate accepts a keyframe whenever this many milliseconds have
|
|
209
|
+
* elapsed since the last accepted keyframe — even if the novelty /
|
|
210
|
+
* overlap threshold wasn't met — so a slow or static pan never goes
|
|
211
|
+
* longer than this without a keyframe. Counts toward `maxKeyframes`
|
|
212
|
+
* (the cap still finalises the capture). `0` disables it. Default
|
|
213
|
+
* `2000` (2 s). Maps to the native gate's `setMaxKeyframeIntervalMs`.
|
|
214
|
+
*/
|
|
215
|
+
maxKeyframeIntervalMs: number;
|
|
216
|
+
|
|
206
217
|
/**
|
|
207
218
|
* Sparse-optical-flow strategy tunables. Consulted only when
|
|
208
219
|
* `mode === 'flow-based'`; safe to omit otherwise. Defaults
|
|
@@ -305,301 +316,17 @@ export const DEFAULT_PANORAMA_SETTINGS: PanoramaSettings = {
|
|
|
305
316
|
warperType: 'plane',
|
|
306
317
|
blenderType: 'multiband',
|
|
307
318
|
seamFinderType: 'graphcut',
|
|
319
|
+
// v0.15 — inscribed-rect crop is OFF by default (bbox crop keeps all
|
|
320
|
+
// stitched content). Opt in with `maxInscribedRectCrop={true}` (or toggle
|
|
321
|
+
// it on in settings) for a clean-cornered rectangle — but it can shrink the
|
|
322
|
+
// output a lot on lopsided / ultra-wide masks, which is why it's opt-in.
|
|
308
323
|
enableMaxInscribedRectCrop: false,
|
|
309
324
|
},
|
|
310
325
|
frameSelection: {
|
|
311
326
|
mode: 'flow-based',
|
|
312
327
|
maxKeyframes: 6,
|
|
313
328
|
overlapThreshold: 0.20,
|
|
329
|
+
maxKeyframeIntervalMs: 2000,
|
|
314
330
|
flow: DEFAULT_FLOW_GATE_SETTINGS,
|
|
315
331
|
},
|
|
316
332
|
};
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
320
|
-
// SlitscanSettings — Layer 2 hosts using the slit-scan engine.
|
|
321
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
322
|
-
|
|
323
|
-
/**
|
|
324
|
-
* Settings for slit-scan stitching engines (`slitscan-rotate`,
|
|
325
|
-
* `slitscan-both`, `firstwins-rectilinear`). Reached via
|
|
326
|
-
* `incremental.start({ engine: '<variant>', config: { ... } })`,
|
|
327
|
-
* NOT via <Camera> (which always uses batch-keyframe). Each
|
|
328
|
-
* sub-tree corresponds to a section of the native `RLISStitcherConfig`
|
|
329
|
-
* the slit-scan engine reads at start.
|
|
330
|
-
*
|
|
331
|
-
* Field-by-field native consumer references are documented in
|
|
332
|
-
* `OpenCVSlitScanStitcher.mm` / `OpenCVIncrementalStitcher.h`.
|
|
333
|
-
*/
|
|
334
|
-
export interface SlitscanSettings extends CaptureBaseSettings {
|
|
335
|
-
/**
|
|
336
|
-
* Which slit-scan variant the engine runs. All three share the
|
|
337
|
-
* same painting + registration + plane configuration; they differ
|
|
338
|
-
* in their internal motion model (rotation-only vs combined
|
|
339
|
-
* translation+rotation, and slit position).
|
|
340
|
-
*
|
|
341
|
-
* • `'slitscan-rotate'` — preferred name; rotation-only
|
|
342
|
-
* motion model.
|
|
343
|
-
* • `'slitscan-both'` — combined translation + rotation
|
|
344
|
-
* motion model.
|
|
345
|
-
* • `'firstwins-rectilinear'` — legacy alias of
|
|
346
|
-
* `'slitscan-rotate'` (V13.0a naming). Accepted natively
|
|
347
|
-
* but new code should prefer the canonical name.
|
|
348
|
-
*/
|
|
349
|
-
variant: 'slitscan-rotate' | 'slitscan-both' | 'firstwins-rectilinear';
|
|
350
|
-
|
|
351
|
-
/** Where the per-accept slit is taken from + how it's blended. */
|
|
352
|
-
painting: SlitscanPaintingSettings;
|
|
353
|
-
|
|
354
|
-
/** Frame-to-frame registration (NCC + RANSAC + triangulation). */
|
|
355
|
-
registration: SlitscanRegistrationSettings;
|
|
356
|
-
|
|
357
|
-
/** Plane projection (ARKit-detected, virtual, or disabled). */
|
|
358
|
-
plane: PlaneProjectionSettings;
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Advanced motion-tuning knobs that the v0.3 modal never exposed.
|
|
362
|
-
* Both are read by the native side
|
|
363
|
-
* (`IncrementalStitcher.swift:1074, 1077`) and have sensible
|
|
364
|
-
* defaults; most consumers can leave this field undefined.
|
|
365
|
-
*/
|
|
366
|
-
advanced?: SlitscanAdvancedSettings;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
export interface SlitscanAdvancedSettings {
|
|
371
|
-
/**
|
|
372
|
-
* Fraction of the pan-axis sensor extent used to compute the
|
|
373
|
-
* per-frame slit width. Range `[0.05, 0.90]`, default 0.70
|
|
374
|
-
* (engine internal). Higher = wider slits = fewer accepts per
|
|
375
|
-
* pan. Set this only if you know what the slit-scan motion
|
|
376
|
-
* model needs for your specific capture geometry.
|
|
377
|
-
* Native key: `kPanAxisFractionRect`.
|
|
378
|
-
*/
|
|
379
|
-
panAxisFractionRect?: number;
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Minimum pan-axis delta (in canvas pixels) between consecutive
|
|
383
|
-
* accepted strips. Acts as a hard floor below which subsequent
|
|
384
|
-
* frames are rejected regardless of NCC scores. Range
|
|
385
|
-
* `[0, 500]`, default 0 (no floor). Native key:
|
|
386
|
-
* `kMinAcceptDeltaPx`.
|
|
387
|
-
*/
|
|
388
|
-
minAcceptDeltaPx?: number;
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
export interface SlitscanPaintingSettings {
|
|
393
|
-
/**
|
|
394
|
-
* How new strips are blended into already-painted canvas pixels.
|
|
395
|
-
*
|
|
396
|
-
* • `'FirstPaintedWins'` (default) — preserve the first frame's
|
|
397
|
-
* content at any pixel; later strips don't overwrite.
|
|
398
|
-
* • `'FeatherBlend'` — alpha-blend new strips into
|
|
399
|
-
* already-painted areas at slit boundaries. Smooths visible
|
|
400
|
-
* seams when many narrow slits stack.
|
|
401
|
-
*/
|
|
402
|
-
paintMode: 'FirstPaintedWins' | 'FeatherBlend';
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Where on the camera frame the per-accept slit is sampled from.
|
|
406
|
-
* For a typical landscape vertical pan tilting DOWN, the leading
|
|
407
|
-
* edge (new content) is at the BOTTOM of the camera frame; for
|
|
408
|
-
* upward tilt, it's at the TOP. `'Center'` is the V13.x default.
|
|
409
|
-
*/
|
|
410
|
-
sliverPosition: 'Center' | 'Bottom' | 'Top';
|
|
411
|
-
|
|
412
|
-
/**
|
|
413
|
-
* When `true`, the very first frame's FULL frame is painted onto
|
|
414
|
-
* the canvas (not just the configured slit clip). Default
|
|
415
|
-
* `true` — gives the panorama a wider initial anchor that
|
|
416
|
-
* subsequent slits extend from. Set false if you want strict
|
|
417
|
-
* slit-only behaviour even on the first frame.
|
|
418
|
-
*/
|
|
419
|
-
firstFrameFullFrame: boolean;
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
export interface SlitscanRegistrationSettings {
|
|
424
|
-
/**
|
|
425
|
-
* 3D triangulation step. Cross-references features across
|
|
426
|
-
* multiple frames to estimate scene depth. Default `false` (off);
|
|
427
|
-
* adds latency, useful for parallax-heavy captures.
|
|
428
|
-
*/
|
|
429
|
-
enableTriangulation: boolean;
|
|
430
|
-
|
|
431
|
-
/**
|
|
432
|
-
* Triangulation accumulator — when `enableTriangulation` is on,
|
|
433
|
-
* keeps a running pose graph across the whole capture. Default
|
|
434
|
-
* `false` (off); needed for multi-shot fusion.
|
|
435
|
-
*/
|
|
436
|
-
enableTriAccumulator: boolean;
|
|
437
|
-
|
|
438
|
-
/**
|
|
439
|
-
* RANSAC homography fit per pair. Adds robustness to feature
|
|
440
|
-
* matching at the cost of a few ms per frame. Default `false`.
|
|
441
|
-
*/
|
|
442
|
-
enableRansacHomography: boolean;
|
|
443
|
-
|
|
444
|
-
/**
|
|
445
|
-
* 1D NCC strip alignment. Present iff enabled. Default
|
|
446
|
-
* undefined (disabled); engine uses pure feature matching.
|
|
447
|
-
*/
|
|
448
|
-
ncc1d?: Ncc1dSettings;
|
|
449
|
-
|
|
450
|
-
/**
|
|
451
|
-
* 2D NCC strip alignment. Present iff enabled. More expensive
|
|
452
|
-
* than 1D NCC; needed for shelf-scan captures with vertical
|
|
453
|
-
* misalignment. Default undefined (disabled).
|
|
454
|
-
*/
|
|
455
|
-
ncc2d?: Ncc2dSettings;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
export interface Ncc1dSettings {
|
|
460
|
-
/**
|
|
461
|
-
* Search radius in working-resolution pixels (along the pan axis).
|
|
462
|
-
* Clamped to `[5, 60]`. Default 15 when the field is set.
|
|
463
|
-
*/
|
|
464
|
-
searchRadius: number;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
export interface Ncc2dSettings {
|
|
469
|
-
/**
|
|
470
|
-
* 2D search margin in pixels (rectangular region around the
|
|
471
|
-
* predicted strip position). Clamped to `[4, 60]`. Default 12.
|
|
472
|
-
*/
|
|
473
|
-
searchMargin: number;
|
|
474
|
-
|
|
475
|
-
/**
|
|
476
|
-
* Minimum NCC score to accept a match. Below this the engine
|
|
477
|
-
* falls back to the predicted (pose-only) position. Clamped
|
|
478
|
-
* to `[0.30, 0.99]`. Default 0.99 (only accept very strong
|
|
479
|
-
* matches; the canvas falls back to pose-only quickly).
|
|
480
|
-
*/
|
|
481
|
-
confidenceThreshold: number;
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* EMA smoothing of the NCC-derived offset across consecutive
|
|
485
|
-
* strips. Present iff enabled. Default undefined. Useful
|
|
486
|
-
* for jittery captures.
|
|
487
|
-
*/
|
|
488
|
-
emaSmoothing?: { alpha: number };
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Pan-axis-lock — when enabled, the NCC offset is constrained
|
|
492
|
-
* to the dominant pan axis (cross-axis movement bounded by
|
|
493
|
-
* `crossAxisLockPx`). Useful when the operator's hand wobble
|
|
494
|
-
* introduces unwanted cross-axis motion. Present iff enabled.
|
|
495
|
-
*/
|
|
496
|
-
panAxisLock?: { crossAxisLockPx: number };
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
export interface PlaneProjectionSettings {
|
|
501
|
-
/**
|
|
502
|
-
* Where the plane the slit-scan projects onto comes from.
|
|
503
|
-
*
|
|
504
|
-
* • `'Disabled'` — no plane projection; engine runs
|
|
505
|
-
* its baseline slit-scan path.
|
|
506
|
-
* • `'ARKitDetected'` — use the first vertical plane that
|
|
507
|
-
* ARKit/ARCore finds AND whose normal
|
|
508
|
-
* aligns with the camera (filtered by
|
|
509
|
-
* `alignmentThreshold`). Requires
|
|
510
|
-
* `captureSource === 'ar'`.
|
|
511
|
-
* • `'Virtual'` — synthesise a plane at a fixed depth
|
|
512
|
-
* (`virtualDepthMeters`) in front of the
|
|
513
|
-
* camera at first-frame pose. No
|
|
514
|
-
* ARKit dependency.
|
|
515
|
-
*/
|
|
516
|
-
source: 'Disabled' | 'ARKitDetected' | 'Virtual';
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* How frames are warped onto the plane. Only consulted when
|
|
520
|
-
* `source !== 'Disabled'`. Default `'Rectified'` for slit-scan.
|
|
521
|
-
*/
|
|
522
|
-
projectionStyle?: 'Trapezoidal' | 'Rectified';
|
|
523
|
-
|
|
524
|
-
/**
|
|
525
|
-
* Depth in metres for `source === 'Virtual'`. Range `[0.3, 5.0]`,
|
|
526
|
-
* default 1.5. Set close to the actual shelf distance for the
|
|
527
|
-
* cleanest projection.
|
|
528
|
-
*/
|
|
529
|
-
virtualDepthMeters?: number;
|
|
530
|
-
|
|
531
|
-
/**
|
|
532
|
-
* Minimum `|planeNormal · cameraForward|` for an ARKit-detected
|
|
533
|
-
* plane to be accepted (when `source === 'ARKitDetected'`).
|
|
534
|
-
* Range `[0, 1]`, default 0.6 (≈ 53° max off-axis). Higher =
|
|
535
|
-
* stricter, only accept very-on-axis planes.
|
|
536
|
-
*/
|
|
537
|
-
alignmentThreshold?: number;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
export const DEFAULT_SLITSCAN_SETTINGS: SlitscanSettings = {
|
|
542
|
-
captureSource: 'ar',
|
|
543
|
-
debug: false,
|
|
544
|
-
variant: 'slitscan-rotate',
|
|
545
|
-
painting: {
|
|
546
|
-
paintMode: 'FirstPaintedWins',
|
|
547
|
-
sliverPosition: 'Bottom',
|
|
548
|
-
firstFrameFullFrame: true,
|
|
549
|
-
},
|
|
550
|
-
registration: {
|
|
551
|
-
enableTriangulation: false,
|
|
552
|
-
enableTriAccumulator: false,
|
|
553
|
-
enableRansacHomography: false,
|
|
554
|
-
// ncc1d / ncc2d omitted — both disabled by default.
|
|
555
|
-
},
|
|
556
|
-
plane: {
|
|
557
|
-
source: 'ARKitDetected',
|
|
558
|
-
projectionStyle: 'Rectified',
|
|
559
|
-
virtualDepthMeters: 1.5,
|
|
560
|
-
alignmentThreshold: 0.6,
|
|
561
|
-
},
|
|
562
|
-
};
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
566
|
-
// HybridSettings — RetaiLens-specific live engine.
|
|
567
|
-
// ═════════════════════════════════════════════════════════════════════
|
|
568
|
-
|
|
569
|
-
/**
|
|
570
|
-
* Settings for the hybrid live-compositing engine
|
|
571
|
-
* (`incremental.start({ engine: 'hybrid', ... })`). Most consumers
|
|
572
|
-
* won't touch this — the hybrid engine is RetaiLens-specific and
|
|
573
|
-
* the public lib's batch-keyframe pipeline is a better fit for
|
|
574
|
-
* general-purpose captures. Exported here for completeness.
|
|
575
|
-
*
|
|
576
|
-
* Important: the hybrid engine has internal preset paths
|
|
577
|
-
* (`OpenCVIncrementalStitcher.mm:139-180`) that hard-set
|
|
578
|
-
* `enableTriangulation`, `enable2dNcc`, `enableRansacHomography`,
|
|
579
|
-
* `planeSource = Disabled`, etc. Code-reviewer flagged that
|
|
580
|
-
* exposing those fields would be misleading — the engine clobbers
|
|
581
|
-
* any overrides. So this type is intentionally minimal: only
|
|
582
|
-
* `projection` is reliably operator-tunable. Hosts that need to
|
|
583
|
-
* reach deeper-level hybrid knobs can pass a raw config dict to
|
|
584
|
-
* `incremental.start()` directly (Layer 2 escape hatch).
|
|
585
|
-
*/
|
|
586
|
-
export interface HybridSettings extends CaptureBaseSettings {
|
|
587
|
-
/**
|
|
588
|
-
* Internal projection during real-time compositing. Independent
|
|
589
|
-
* from the panorama-stitcher's warperType (which doesn't apply
|
|
590
|
-
* to the hybrid engine — its output is the live canvas directly).
|
|
591
|
-
*
|
|
592
|
-
* Note: only effective in the rotation-only preset path (hybrid
|
|
593
|
-
* preset 1). In the other hybrid presets the engine forces
|
|
594
|
-
* Planar internally regardless of this setting. Native source:
|
|
595
|
-
* `OpenCVIncrementalStitcher.mm:146,161,180`.
|
|
596
|
-
*/
|
|
597
|
-
projection: 'Cylindrical' | 'Planar';
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
export const DEFAULT_HYBRID_SETTINGS: HybridSettings = {
|
|
602
|
-
captureSource: 'ar',
|
|
603
|
-
debug: false,
|
|
604
|
-
projection: 'Planar',
|
|
605
|
-
};
|